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 tool = entry.tool.as_deref().unwrap_or("claude-code");
236 let artifact_path = manifest
237 .get_artifact_resource_path(tool, *resource_type)
238 .expect("Resource type should be supported by configured tools");
239 let target_dir = std::borrow::Cow::Owned(artifact_path.display().to_string());
240
241 all_entries.push((entry, target_dir));
242 }
243 }
244
245 all_entries
246 }
247
248 pub fn find_resource_by_name<'a>(
256 lockfile: &'a LockFile,
257 name: &str,
258 ) -> Option<(ResourceType, &'a LockedResource)> {
259 for resource_type in ResourceType::all() {
260 if let Some(entry) =
261 resource_type.get_lockfile_entries(lockfile).iter().find(|e| e.name == name)
262 {
263 return Some((*resource_type, entry));
264 }
265 }
266 None
267 }
268
269 pub fn find_resource_by_name_and_source<'a>(
282 lockfile: &'a LockFile,
283 name: &str,
284 source: Option<&str>,
285 ) -> Option<(ResourceType, &'a LockedResource)> {
286 for resource_type in ResourceType::all() {
287 if let Some(entry) = resource_type
288 .get_lockfile_entries(lockfile)
289 .iter()
290 .find(|e| e.name == name && e.source.as_deref() == source)
291 {
292 return Some((*resource_type, entry));
293 }
294 }
295 None
296 }
297
298 pub fn count_total_resources(lockfile: &LockFile) -> usize {
300 ResourceType::all().iter().map(|rt| rt.get_lockfile_entries(lockfile).len()).sum()
301 }
302
303 pub fn count_manifest_dependencies(manifest: &Manifest) -> usize {
305 ResourceType::all().iter().map(|rt| rt.get_manifest_entries(manifest).len()).sum()
306 }
307
308 pub fn has_resources(lockfile: &LockFile) -> bool {
310 ResourceType::all().iter().any(|rt| !rt.get_lockfile_entries(lockfile).is_empty())
311 }
312
313 pub fn get_all_resource_names(lockfile: &LockFile) -> Vec<String> {
315 let mut names = Vec::new();
316 for resource_type in ResourceType::all() {
317 for entry in resource_type.get_lockfile_entries(lockfile) {
318 names.push(entry.name.clone());
319 }
320 }
321 names
322 }
323
324 pub fn get_resources_by_source<'a>(
326 lockfile: &'a LockFile,
327 resource_type: ResourceType,
328 source: &str,
329 ) -> Vec<&'a LockedResource> {
330 resource_type
331 .get_lockfile_entries(lockfile)
332 .iter()
333 .filter(|e| e.source.as_deref() == Some(source))
334 .collect()
335 }
336
337 pub fn for_each_resource<F>(lockfile: &LockFile, mut f: F)
339 where
340 F: FnMut(ResourceType, &LockedResource),
341 {
342 for resource_type in ResourceType::all() {
343 for entry in resource_type.get_lockfile_entries(lockfile) {
344 f(*resource_type, entry);
345 }
346 }
347 }
348
349 pub fn map_resources<T, F>(lockfile: &LockFile, mut f: F) -> Vec<T>
351 where
352 F: FnMut(ResourceType, &LockedResource) -> T,
353 {
354 let mut results = Vec::new();
355 Self::for_each_resource(lockfile, |rt, entry| {
356 results.push(f(rt, entry));
357 });
358 results
359 }
360
361 pub fn filter_resources<F>(
363 lockfile: &LockFile,
364 mut predicate: F,
365 ) -> Vec<(ResourceType, LockedResource)>
366 where
367 F: FnMut(ResourceType, &LockedResource) -> bool,
368 {
369 let mut results = Vec::new();
370 Self::for_each_resource(lockfile, |rt, entry| {
371 if predicate(rt, entry) {
372 results.push((rt, entry.clone()));
373 }
374 });
375 results
376 }
377
378 pub fn group_by_source(
380 lockfile: &LockFile,
381 ) -> std::collections::HashMap<String, Vec<(ResourceType, LockedResource)>> {
382 let mut groups = std::collections::HashMap::new();
383
384 Self::for_each_resource(lockfile, |rt, entry| {
385 if let Some(ref source) = entry.source {
386 groups.entry(source.clone()).or_insert_with(Vec::new).push((rt, entry.clone()));
387 }
388 });
389
390 groups
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397 use crate::lockfile::{LockFile, LockedResource};
398 use crate::manifest::Manifest;
399 use crate::utils::normalize_path_for_storage;
400
401 fn create_test_lockfile() -> LockFile {
402 let mut lockfile = LockFile::new();
403
404 lockfile.agents.push(LockedResource {
405 name: "test-agent".to_string(),
406 source: Some("community".to_string()),
407 url: Some("https://github.com/test/repo.git".to_string()),
408 path: "agents/test.md".to_string(),
409 version: Some("v1.0.0".to_string()),
410 resolved_commit: Some("abc123".to_string()),
411 checksum: "sha256:abc".to_string(),
412 installed_at: ".claude/agents/test-agent.md".to_string(),
413 dependencies: vec![],
414 resource_type: crate::core::ResourceType::Agent,
415
416 tool: Some("claude-code".to_string()),
417 manifest_alias: None,
418 applied_patches: std::collections::HashMap::new(),
419 install: None,
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: Some("claude-code".to_string()),
435 manifest_alias: None,
436 applied_patches: std::collections::HashMap::new(),
437 install: None,
438 });
439
440 lockfile
441 }
442
443 fn create_test_manifest() -> Manifest {
444 Manifest::default()
445 }
446
447 fn create_multi_resource_lockfile() -> LockFile {
448 let mut lockfile = LockFile::new();
449
450 lockfile.agents.push(LockedResource {
452 name: "agent1".to_string(),
453 source: Some("source1".to_string()),
454 url: Some("https://github.com/source1/repo.git".to_string()),
455 path: "agents/agent1.md".to_string(),
456 version: Some("v1.0.0".to_string()),
457 resolved_commit: Some("abc123".to_string()),
458 checksum: "sha256:abc1".to_string(),
459 installed_at: ".claude/agents/agent1.md".to_string(),
460 dependencies: vec![],
461 resource_type: crate::core::ResourceType::Agent,
462
463 tool: Some("claude-code".to_string()),
464 manifest_alias: None,
465 applied_patches: std::collections::HashMap::new(),
466 install: None,
467 });
468
469 lockfile.agents.push(LockedResource {
470 name: "agent2".to_string(),
471 source: Some("source2".to_string()),
472 url: Some("https://github.com/source2/repo.git".to_string()),
473 path: "agents/agent2.md".to_string(),
474 version: Some("v2.0.0".to_string()),
475 resolved_commit: Some("def456".to_string()),
476 checksum: "sha256:def2".to_string(),
477 installed_at: ".claude/agents/agent2.md".to_string(),
478 dependencies: vec![],
479 resource_type: crate::core::ResourceType::Agent,
480
481 tool: Some("claude-code".to_string()),
482 manifest_alias: None,
483 applied_patches: std::collections::HashMap::new(),
484 install: None,
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
500 tool: Some("claude-code".to_string()),
501 manifest_alias: None,
502 applied_patches: std::collections::HashMap::new(),
503 install: None,
504 });
505
506 lockfile.scripts.push(LockedResource {
508 name: "script1".to_string(),
509 source: Some("source1".to_string()),
510 url: Some("https://github.com/source1/repo.git".to_string()),
511 path: "scripts/build.sh".to_string(),
512 version: Some("v1.0.0".to_string()),
513 resolved_commit: Some("jkl012".to_string()),
514 checksum: "sha256:jkl4".to_string(),
515 installed_at: ".claude/scripts/script1.sh".to_string(),
516 dependencies: vec![],
517 resource_type: crate::core::ResourceType::Script,
518
519 tool: Some("claude-code".to_string()),
520 manifest_alias: None,
521 applied_patches: std::collections::HashMap::new(),
522 install: None,
523 });
524
525 lockfile.hooks.push(LockedResource {
527 name: "hook1".to_string(),
528 source: Some("source2".to_string()),
529 url: Some("https://github.com/source2/repo.git".to_string()),
530 path: "hooks/pre-commit.json".to_string(),
531 version: Some("v1.0.0".to_string()),
532 resolved_commit: Some("mno345".to_string()),
533 checksum: "sha256:mno5".to_string(),
534 installed_at: ".claude/hooks/hook1.json".to_string(),
535 dependencies: vec![],
536 resource_type: crate::core::ResourceType::Hook,
537
538 tool: Some("claude-code".to_string()),
539 manifest_alias: None,
540 applied_patches: std::collections::HashMap::new(),
541 install: None,
542 });
543
544 lockfile.mcp_servers.push(LockedResource {
546 name: "mcp1".to_string(),
547 source: Some("source1".to_string()),
548 url: Some("https://github.com/source1/repo.git".to_string()),
549 path: "mcp-servers/filesystem.json".to_string(),
550 version: Some("v1.0.0".to_string()),
551 resolved_commit: Some("pqr678".to_string()),
552 checksum: "sha256:pqr6".to_string(),
553 installed_at: ".mcp-servers/mcp1.json".to_string(),
554 dependencies: vec![],
555 resource_type: crate::core::ResourceType::McpServer,
556
557 tool: Some("claude-code".to_string()),
558 manifest_alias: None,
559 applied_patches: std::collections::HashMap::new(),
560 install: None,
561 });
562
563 lockfile.snippets.push(LockedResource {
565 name: "local-snippet".to_string(),
566 source: None,
567 url: None,
568 path: "local/snippet.md".to_string(),
569 version: None,
570 resolved_commit: None,
571 checksum: "sha256:local".to_string(),
572 installed_at: ".agpm/snippets/local-snippet.md".to_string(),
573 dependencies: vec![],
574 resource_type: crate::core::ResourceType::Snippet,
575
576 tool: Some("claude-code".to_string()),
577 manifest_alias: None,
578 applied_patches: std::collections::HashMap::new(),
579 install: None,
580 });
581
582 lockfile
583 }
584
585 #[test]
586 fn test_resource_type_all() {
587 let all_types = ResourceType::all();
588 assert_eq!(all_types.len(), 6);
589 assert_eq!(all_types[0], ResourceType::Agent);
591 assert_eq!(all_types[1], ResourceType::Snippet);
592 assert_eq!(all_types[2], ResourceType::Command);
593 assert_eq!(all_types[3], ResourceType::McpServer);
594 assert_eq!(all_types[4], ResourceType::Script);
595 assert_eq!(all_types[5], ResourceType::Hook);
596 }
597
598 #[test]
599 fn test_get_lockfile_entries_mut() {
600 let mut lockfile = create_test_lockfile();
601
602 let mut agent_type = ResourceType::Agent;
604 let entries = agent_type.get_lockfile_entries_mut(&mut lockfile);
605 assert_eq!(entries.len(), 1);
606 assert_eq!(entries[0].name, "test-agent");
607
608 entries.push(LockedResource {
610 name: "new-agent".to_string(),
611 source: Some("test".to_string()),
612 url: Some("https://example.com/repo.git".to_string()),
613 path: "agents/new.md".to_string(),
614 version: Some("v1.0.0".to_string()),
615 resolved_commit: Some("xyz789".to_string()),
616 checksum: "sha256:xyz".to_string(),
617 installed_at: ".claude/agents/new-agent.md".to_string(),
618 dependencies: vec![],
619 resource_type: crate::core::ResourceType::Agent,
620
621 tool: Some("claude-code".to_string()),
622 manifest_alias: None,
623 applied_patches: std::collections::HashMap::new(),
624 install: None,
625 });
626
627 assert_eq!(lockfile.agents.len(), 2);
629 assert_eq!(lockfile.agents[1].name, "new-agent");
630
631 let mut snippet_type = ResourceType::Snippet;
633 let snippet_entries = snippet_type.get_lockfile_entries_mut(&mut lockfile);
634 assert_eq!(snippet_entries.len(), 1);
635
636 let mut command_type = ResourceType::Command;
637 let command_entries = command_type.get_lockfile_entries_mut(&mut lockfile);
638 assert_eq!(command_entries.len(), 0);
639
640 let mut script_type = ResourceType::Script;
641 let script_entries = script_type.get_lockfile_entries_mut(&mut lockfile);
642 assert_eq!(script_entries.len(), 0);
643
644 let mut hook_type = ResourceType::Hook;
645 let hook_entries = hook_type.get_lockfile_entries_mut(&mut lockfile);
646 assert_eq!(hook_entries.len(), 0);
647
648 let mut mcp_type = ResourceType::McpServer;
649 let mcp_entries = mcp_type.get_lockfile_entries_mut(&mut lockfile);
650 assert_eq!(mcp_entries.len(), 0);
651 }
652
653 #[test]
654 fn test_collect_all_entries() {
655 let lockfile = create_test_lockfile();
656 let manifest = create_test_manifest();
657
658 let entries = ResourceIterator::collect_all_entries(&lockfile, &manifest);
659 assert_eq!(entries.len(), 2);
660
661 assert_eq!(entries[0].0.name, "test-agent");
662 assert_eq!(normalize_path_for_storage(entries[0].1.as_ref()), ".claude/agents");
664
665 assert_eq!(entries[1].0.name, "test-snippet");
666 assert_eq!(normalize_path_for_storage(entries[1].1.as_ref()), ".claude/snippets");
669 }
670
671 #[test]
672 fn test_collect_all_entries_empty_lockfile() {
673 let empty_lockfile = LockFile::new();
674 let manifest = create_test_manifest();
675
676 let entries = ResourceIterator::collect_all_entries(&empty_lockfile, &manifest);
677 assert_eq!(entries.len(), 0);
678 }
679
680 #[test]
681 fn test_collect_all_entries_multiple_resources() {
682 let lockfile = create_multi_resource_lockfile();
683 let manifest = create_test_manifest();
684
685 let entries = ResourceIterator::collect_all_entries(&lockfile, &manifest);
686
687 assert_eq!(entries.len(), 5);
690
691 let mut found_types = std::collections::HashSet::new();
693 for (resource, _) in &entries {
694 match resource.name.as_str() {
695 "agent1" | "agent2" => {
696 found_types.insert("agent");
697 }
698 "local-snippet" => {
699 found_types.insert("snippet");
700 }
701 "command1" => {
702 found_types.insert("command");
703 }
704 "script1" => {
705 found_types.insert("script");
706 }
707 "hook1" | "mcp1" => {
709 panic!("Hooks and MCP servers should not be in collected entries");
710 }
711 _ => {}
712 }
713 }
714
715 assert_eq!(found_types.len(), 4);
716 }
717
718 #[test]
719 fn test_find_resource_by_name() {
720 let lockfile = create_test_lockfile();
721
722 let result = ResourceIterator::find_resource_by_name(&lockfile, "test-agent");
723 assert!(result.is_some());
724 let (rt, resource) = result.unwrap();
725 assert_eq!(rt, ResourceType::Agent);
726 assert_eq!(resource.name, "test-agent");
727
728 let result = ResourceIterator::find_resource_by_name(&lockfile, "nonexistent");
729 assert!(result.is_none());
730 }
731
732 #[test]
733 fn test_find_resource_by_name_multiple_types() {
734 let lockfile = create_multi_resource_lockfile();
735
736 let result = ResourceIterator::find_resource_by_name(&lockfile, "agent1");
738 assert!(result.is_some());
739 let (rt, resource) = result.unwrap();
740 assert_eq!(rt, ResourceType::Agent);
741 assert_eq!(resource.name, "agent1");
742
743 let result = ResourceIterator::find_resource_by_name(&lockfile, "command1");
745 assert!(result.is_some());
746 let (rt, resource) = result.unwrap();
747 assert_eq!(rt, ResourceType::Command);
748 assert_eq!(resource.name, "command1");
749
750 let result = ResourceIterator::find_resource_by_name(&lockfile, "script1");
752 assert!(result.is_some());
753 let (rt, resource) = result.unwrap();
754 assert_eq!(rt, ResourceType::Script);
755 assert_eq!(resource.name, "script1");
756
757 let result = ResourceIterator::find_resource_by_name(&lockfile, "hook1");
759 assert!(result.is_some());
760 let (rt, resource) = result.unwrap();
761 assert_eq!(rt, ResourceType::Hook);
762 assert_eq!(resource.name, "hook1");
763
764 let result = ResourceIterator::find_resource_by_name(&lockfile, "mcp1");
766 assert!(result.is_some());
767 let (rt, resource) = result.unwrap();
768 assert_eq!(rt, ResourceType::McpServer);
769 assert_eq!(resource.name, "mcp1");
770
771 let result = ResourceIterator::find_resource_by_name(&lockfile, "local-snippet");
773 assert!(result.is_some());
774 let (rt, resource) = result.unwrap();
775 assert_eq!(rt, ResourceType::Snippet);
776 assert_eq!(resource.name, "local-snippet");
777 assert!(resource.source.is_none());
778 }
779
780 #[test]
781 fn test_count_and_has_resources() {
782 let lockfile = create_test_lockfile();
783 assert_eq!(ResourceIterator::count_total_resources(&lockfile), 2);
784 assert!(ResourceIterator::has_resources(&lockfile));
785
786 let empty_lockfile = LockFile::new();
787 assert_eq!(ResourceIterator::count_total_resources(&empty_lockfile), 0);
788 assert!(!ResourceIterator::has_resources(&empty_lockfile));
789
790 let multi_lockfile = create_multi_resource_lockfile();
791 assert_eq!(ResourceIterator::count_total_resources(&multi_lockfile), 7);
792 assert!(ResourceIterator::has_resources(&multi_lockfile));
793 }
794
795 #[test]
796 fn test_get_all_resource_names() {
797 let lockfile = create_test_lockfile();
798 let names = ResourceIterator::get_all_resource_names(&lockfile);
799
800 assert_eq!(names.len(), 2);
801 assert!(names.contains(&"test-agent".to_string()));
802 assert!(names.contains(&"test-snippet".to_string()));
803 }
804
805 #[test]
806 fn test_get_all_resource_names_empty() {
807 let empty_lockfile = LockFile::new();
808 let names = ResourceIterator::get_all_resource_names(&empty_lockfile);
809 assert_eq!(names.len(), 0);
810 }
811
812 #[test]
813 fn test_get_all_resource_names_multiple() {
814 let lockfile = create_multi_resource_lockfile();
815 let names = ResourceIterator::get_all_resource_names(&lockfile);
816
817 assert_eq!(names.len(), 7);
818 assert!(names.contains(&"agent1".to_string()));
819 assert!(names.contains(&"agent2".to_string()));
820 assert!(names.contains(&"local-snippet".to_string()));
821 assert!(names.contains(&"command1".to_string()));
822 assert!(names.contains(&"script1".to_string()));
823 assert!(names.contains(&"hook1".to_string()));
824 assert!(names.contains(&"mcp1".to_string()));
825 }
826
827 #[test]
828 fn test_get_resources_by_source() {
829 let lockfile = create_multi_resource_lockfile();
830
831 let source1_resources =
833 ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Agent, "source1");
834 assert_eq!(source1_resources.len(), 1);
835 assert_eq!(source1_resources[0].name, "agent1");
836
837 let source1_commands =
838 ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Command, "source1");
839 assert_eq!(source1_commands.len(), 1);
840 assert_eq!(source1_commands[0].name, "command1");
841
842 let source1_scripts =
843 ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Script, "source1");
844 assert_eq!(source1_scripts.len(), 1);
845 assert_eq!(source1_scripts[0].name, "script1");
846
847 let source1_mcps = ResourceIterator::get_resources_by_source(
848 &lockfile,
849 ResourceType::McpServer,
850 "source1",
851 );
852 assert_eq!(source1_mcps.len(), 1);
853 assert_eq!(source1_mcps[0].name, "mcp1");
854
855 let source2_agents =
857 ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Agent, "source2");
858 assert_eq!(source2_agents.len(), 1);
859 assert_eq!(source2_agents[0].name, "agent2");
860
861 let source2_hooks =
862 ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Hook, "source2");
863 assert_eq!(source2_hooks.len(), 1);
864 assert_eq!(source2_hooks[0].name, "hook1");
865
866 let nonexistent = ResourceIterator::get_resources_by_source(
868 &lockfile,
869 ResourceType::Agent,
870 "nonexistent",
871 );
872 assert_eq!(nonexistent.len(), 0);
873
874 let source1_snippets =
876 ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Snippet, "source1");
877 assert_eq!(source1_snippets.len(), 0);
878 }
879
880 #[test]
881 fn test_for_each_resource() {
882 let lockfile = create_multi_resource_lockfile();
883 let mut visited_resources = Vec::new();
884
885 ResourceIterator::for_each_resource(&lockfile, |resource_type, resource| {
886 visited_resources.push((resource_type, resource.name.clone()));
887 });
888
889 assert_eq!(visited_resources.len(), 7);
890
891 let expected_resources = vec![
893 (ResourceType::Agent, "agent1".to_string()),
894 (ResourceType::Agent, "agent2".to_string()),
895 (ResourceType::Snippet, "local-snippet".to_string()),
896 (ResourceType::Command, "command1".to_string()),
897 (ResourceType::Script, "script1".to_string()),
898 (ResourceType::Hook, "hook1".to_string()),
899 (ResourceType::McpServer, "mcp1".to_string()),
900 ];
901
902 for expected in expected_resources {
903 assert!(visited_resources.contains(&expected));
904 }
905 }
906
907 #[test]
908 fn test_for_each_resource_empty() {
909 let empty_lockfile = LockFile::new();
910 let mut count = 0;
911
912 ResourceIterator::for_each_resource(&empty_lockfile, |_, _| {
913 count += 1;
914 });
915
916 assert_eq!(count, 0);
917 }
918
919 #[test]
920 fn test_map_resources() {
921 let lockfile = create_multi_resource_lockfile();
922
923 let names = ResourceIterator::map_resources(&lockfile, |_, resource| resource.name.clone());
925
926 assert_eq!(names.len(), 7);
927 assert!(names.contains(&"agent1".to_string()));
928 assert!(names.contains(&"agent2".to_string()));
929 assert!(names.contains(&"local-snippet".to_string()));
930 assert!(names.contains(&"command1".to_string()));
931 assert!(names.contains(&"script1".to_string()));
932 assert!(names.contains(&"hook1".to_string()));
933 assert!(names.contains(&"mcp1".to_string()));
934
935 let type_name_pairs =
937 ResourceIterator::map_resources(&lockfile, |resource_type, resource| {
938 format!("{}:{}", resource_type, resource.name)
939 });
940
941 assert_eq!(type_name_pairs.len(), 7);
942 assert!(type_name_pairs.contains(&"agent:agent1".to_string()));
943 assert!(type_name_pairs.contains(&"agent:agent2".to_string()));
944 assert!(type_name_pairs.contains(&"snippet:local-snippet".to_string()));
945 assert!(type_name_pairs.contains(&"command:command1".to_string()));
946 assert!(type_name_pairs.contains(&"script:script1".to_string()));
947 assert!(type_name_pairs.contains(&"hook:hook1".to_string()));
948 assert!(type_name_pairs.contains(&"mcp-server:mcp1".to_string()));
949 }
950
951 #[test]
952 fn test_map_resources_empty() {
953 let empty_lockfile = LockFile::new();
954
955 let results =
956 ResourceIterator::map_resources(&empty_lockfile, |_, resource| resource.name.clone());
957
958 assert_eq!(results.len(), 0);
959 }
960
961 #[test]
962 fn test_filter_resources() {
963 let lockfile = create_multi_resource_lockfile();
964
965 let source1_resources = ResourceIterator::filter_resources(&lockfile, |_, resource| {
967 resource.source.as_deref() == Some("source1")
968 });
969
970 assert_eq!(source1_resources.len(), 4); let source1_names: Vec<String> =
972 source1_resources.iter().map(|(_, r)| r.name.clone()).collect();
973 assert!(source1_names.contains(&"agent1".to_string()));
974 assert!(source1_names.contains(&"command1".to_string()));
975 assert!(source1_names.contains(&"script1".to_string()));
976 assert!(source1_names.contains(&"mcp1".to_string()));
977
978 let agents = ResourceIterator::filter_resources(&lockfile, |resource_type, _| {
980 resource_type == ResourceType::Agent
981 });
982
983 assert_eq!(agents.len(), 2); let agent_names: Vec<String> = agents.iter().map(|(_, r)| r.name.clone()).collect();
985 assert!(agent_names.contains(&"agent1".to_string()));
986 assert!(agent_names.contains(&"agent2".to_string()));
987
988 let no_source_resources =
990 ResourceIterator::filter_resources(&lockfile, |_, resource| resource.source.is_none());
991
992 assert_eq!(no_source_resources.len(), 1); assert_eq!(no_source_resources[0].1.name, "local-snippet");
994
995 let v1_resources = ResourceIterator::filter_resources(&lockfile, |_, resource| {
997 resource.version.as_deref().unwrap_or("").starts_with("v1.")
998 });
999
1000 assert_eq!(v1_resources.len(), 5); let no_matches = ResourceIterator::filter_resources(&lockfile, |_, resource| {
1004 resource.name == "nonexistent"
1005 });
1006
1007 assert_eq!(no_matches.len(), 0);
1008 }
1009
1010 #[test]
1011 fn test_filter_resources_empty() {
1012 let empty_lockfile = LockFile::new();
1013
1014 let results = ResourceIterator::filter_resources(&empty_lockfile, |_, _| true);
1015 assert_eq!(results.len(), 0);
1016 }
1017
1018 #[test]
1019 fn test_group_by_source() {
1020 let lockfile = create_multi_resource_lockfile();
1021
1022 let groups = ResourceIterator::group_by_source(&lockfile);
1023
1024 assert_eq!(groups.len(), 2); let source1_group = groups.get("source1").unwrap();
1028 assert_eq!(source1_group.len(), 4); let source1_names: Vec<String> =
1031 source1_group.iter().map(|(_, r)| r.name.clone()).collect();
1032 assert!(source1_names.contains(&"agent1".to_string()));
1033 assert!(source1_names.contains(&"command1".to_string()));
1034 assert!(source1_names.contains(&"script1".to_string()));
1035 assert!(source1_names.contains(&"mcp1".to_string()));
1036
1037 let source2_group = groups.get("source2").unwrap();
1039 assert_eq!(source2_group.len(), 2); let source2_names: Vec<String> =
1042 source2_group.iter().map(|(_, r)| r.name.clone()).collect();
1043 assert!(source2_names.contains(&"agent2".to_string()));
1044 assert!(source2_names.contains(&"hook1".to_string()));
1045
1046 assert!(!groups.contains_key(""));
1048 }
1049
1050 #[test]
1051 fn test_group_by_source_empty() {
1052 let empty_lockfile = LockFile::new();
1053
1054 let groups = ResourceIterator::group_by_source(&empty_lockfile);
1055 assert_eq!(groups.len(), 0);
1056 }
1057
1058 #[test]
1059 fn test_group_by_source_no_sources() {
1060 let mut lockfile = LockFile::new();
1061
1062 lockfile.agents.push(LockedResource {
1064 name: "local-agent".to_string(),
1065 source: None,
1066 url: None,
1067 path: "local/agent.md".to_string(),
1068 version: None,
1069 resolved_commit: None,
1070 checksum: "sha256:local".to_string(),
1071 installed_at: ".claude/agents/local-agent.md".to_string(),
1072 dependencies: vec![],
1073 resource_type: crate::core::ResourceType::Agent,
1074
1075 tool: Some("claude-code".to_string()),
1076 manifest_alias: None,
1077 applied_patches: std::collections::HashMap::new(),
1078 install: None,
1079 });
1080
1081 let groups = ResourceIterator::group_by_source(&lockfile);
1082 assert_eq!(groups.len(), 0); }
1084
1085 #[test]
1086 fn test_resource_type_ext() {
1087 let lockfile = create_test_lockfile();
1088
1089 assert_eq!(ResourceType::Agent.get_lockfile_entries(&lockfile).len(), 1);
1090 assert_eq!(ResourceType::Snippet.get_lockfile_entries(&lockfile).len(), 1);
1091 assert_eq!(ResourceType::Command.get_lockfile_entries(&lockfile).len(), 0);
1092 }
1093
1094 #[test]
1095 fn test_resource_type_ext_all_types() {
1096 let lockfile = create_multi_resource_lockfile();
1097
1098 assert_eq!(ResourceType::Agent.get_lockfile_entries(&lockfile).len(), 2);
1099 assert_eq!(ResourceType::Snippet.get_lockfile_entries(&lockfile).len(), 1);
1100 assert_eq!(ResourceType::Command.get_lockfile_entries(&lockfile).len(), 1);
1101 assert_eq!(ResourceType::Script.get_lockfile_entries(&lockfile).len(), 1);
1102 assert_eq!(ResourceType::Hook.get_lockfile_entries(&lockfile).len(), 1);
1103 assert_eq!(ResourceType::McpServer.get_lockfile_entries(&lockfile).len(), 1);
1104 }
1105}