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 approximate_token_count: None,
415 });
416
417 lockfile.snippets.push(LockedResource {
418 name: "test-snippet".to_string(),
419 source: Some("community".to_string()),
420 url: Some("https://github.com/test/repo.git".to_string()),
421 path: "snippets/test.md".to_string(),
422 version: Some("v1.0.0".to_string()),
423 resolved_commit: Some("def456".to_string()),
424 checksum: "sha256:def".to_string(),
425 installed_at: ".claude/snippets/test-snippet.md".to_string(),
426 dependencies: vec![],
427 resource_type: crate::core::ResourceType::Snippet,
428 context_checksum: None,
429 tool: Some("claude-code".to_string()),
430 manifest_alias: None,
431 applied_patches: std::collections::BTreeMap::new(),
432 install: None,
433 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
434 is_private: false,
435 approximate_token_count: None,
436 });
437
438 lockfile
439 }
440
441 fn create_test_manifest() -> Manifest {
442 Manifest::default()
443 }
444
445 fn create_multi_resource_lockfile() -> LockFile {
446 let mut lockfile = LockFile::new();
447
448 lockfile.agents.push(LockedResource {
450 name: "agent1".to_string(),
451 source: Some("source1".to_string()),
452 url: Some("https://github.com/source1/repo.git".to_string()),
453 path: "agents/agent1.md".to_string(),
454 version: Some("v1.0.0".to_string()),
455 resolved_commit: Some("abc123".to_string()),
456 checksum: "sha256:abc1".to_string(),
457 installed_at: ".claude/agents/agent1.md".to_string(),
458 dependencies: vec![],
459 resource_type: crate::core::ResourceType::Agent,
460 context_checksum: None,
461 tool: Some("claude-code".to_string()),
462 manifest_alias: None,
463 applied_patches: std::collections::BTreeMap::new(),
464 install: None,
465 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
466 is_private: false,
467 approximate_token_count: None,
468 });
469
470 lockfile.agents.push(LockedResource {
471 name: "agent2".to_string(),
472 source: Some("source2".to_string()),
473 url: Some("https://github.com/source2/repo.git".to_string()),
474 path: "agents/agent2.md".to_string(),
475 version: Some("v2.0.0".to_string()),
476 resolved_commit: Some("def456".to_string()),
477 checksum: "sha256:def2".to_string(),
478 installed_at: ".claude/agents/agent2.md".to_string(),
479 dependencies: vec![],
480 resource_type: crate::core::ResourceType::Agent,
481 context_checksum: None,
482 tool: Some("claude-code".to_string()),
483 manifest_alias: None,
484 applied_patches: std::collections::BTreeMap::new(),
485 install: None,
486 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
487 is_private: false,
488 approximate_token_count: None,
489 });
490
491 lockfile.commands.push(LockedResource {
493 name: "command1".to_string(),
494 source: Some("source1".to_string()),
495 url: Some("https://github.com/source1/repo.git".to_string()),
496 path: "commands/command1.md".to_string(),
497 version: Some("v1.1.0".to_string()),
498 resolved_commit: Some("ghi789".to_string()),
499 checksum: "sha256:ghi3".to_string(),
500 installed_at: ".claude/commands/command1.md".to_string(),
501 dependencies: vec![],
502 resource_type: crate::core::ResourceType::Command,
503 context_checksum: None,
504 tool: Some("claude-code".to_string()),
505 manifest_alias: None,
506 applied_patches: std::collections::BTreeMap::new(),
507 install: None,
508 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
509 is_private: false,
510 approximate_token_count: None,
511 });
512
513 lockfile.scripts.push(LockedResource {
515 name: "script1".to_string(),
516 source: Some("source1".to_string()),
517 url: Some("https://github.com/source1/repo.git".to_string()),
518 path: "scripts/build.sh".to_string(),
519 version: Some("v1.0.0".to_string()),
520 resolved_commit: Some("jkl012".to_string()),
521 checksum: "sha256:jkl4".to_string(),
522 installed_at: ".claude/scripts/script1.sh".to_string(),
523 dependencies: vec![],
524 resource_type: crate::core::ResourceType::Script,
525 context_checksum: None,
526 tool: Some("claude-code".to_string()),
527 manifest_alias: None,
528 applied_patches: std::collections::BTreeMap::new(),
529 install: None,
530 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
531 is_private: false,
532 approximate_token_count: None,
533 });
534
535 lockfile.hooks.push(LockedResource {
537 name: "hook1".to_string(),
538 source: Some("source2".to_string()),
539 url: Some("https://github.com/source2/repo.git".to_string()),
540 path: "hooks/pre-commit.json".to_string(),
541 version: Some("v1.0.0".to_string()),
542 resolved_commit: Some("mno345".to_string()),
543 checksum: "sha256:mno5".to_string(),
544 installed_at: ".claude/hooks/hook1.json".to_string(),
545 dependencies: vec![],
546 resource_type: crate::core::ResourceType::Hook,
547 context_checksum: None,
548 tool: Some("claude-code".to_string()),
549 manifest_alias: None,
550 applied_patches: std::collections::BTreeMap::new(),
551 install: None,
552 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
553 is_private: false,
554 approximate_token_count: None,
555 });
556
557 lockfile.mcp_servers.push(LockedResource {
559 name: "mcp1".to_string(),
560 source: Some("source1".to_string()),
561 url: Some("https://github.com/source1/repo.git".to_string()),
562 path: "mcp-servers/filesystem.json".to_string(),
563 version: Some("v1.0.0".to_string()),
564 resolved_commit: Some("pqr678".to_string()),
565 checksum: "sha256:pqr6".to_string(),
566 installed_at: ".mcp-servers/mcp1.json".to_string(),
567 dependencies: vec![],
568 resource_type: crate::core::ResourceType::McpServer,
569 context_checksum: None,
570 tool: Some("claude-code".to_string()),
571 manifest_alias: None,
572 applied_patches: std::collections::BTreeMap::new(),
573 install: None,
574 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
575 is_private: false,
576 approximate_token_count: None,
577 });
578
579 lockfile.snippets.push(LockedResource {
581 name: "local-snippet".to_string(),
582 source: None,
583 url: None,
584 path: "local/snippet.md".to_string(),
585 version: None,
586 resolved_commit: None,
587 checksum: "sha256:local".to_string(),
588 installed_at: ".agpm/snippets/local-snippet.md".to_string(),
589 dependencies: vec![],
590 resource_type: crate::core::ResourceType::Snippet,
591 context_checksum: None,
592 tool: Some("claude-code".to_string()),
593 manifest_alias: None,
594 applied_patches: std::collections::BTreeMap::new(),
595 install: None,
596 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
597 is_private: false,
598 approximate_token_count: None,
599 });
600
601 lockfile
602 }
603
604 #[test]
605 fn test_resource_type_all() {
606 let all_types = ResourceType::all();
607 assert_eq!(all_types.len(), 7);
608 assert_eq!(all_types[0], ResourceType::Agent);
610 assert_eq!(all_types[1], ResourceType::Snippet);
611 assert_eq!(all_types[2], ResourceType::Command);
612 assert_eq!(all_types[3], ResourceType::McpServer);
613 assert_eq!(all_types[4], ResourceType::Script);
614 assert_eq!(all_types[5], ResourceType::Hook);
615 assert_eq!(all_types[6], ResourceType::Skill);
616 }
617
618 #[test]
619 fn test_get_lockfile_entries_mut() {
620 let mut lockfile = create_test_lockfile();
621
622 let mut agent_type = ResourceType::Agent;
624 let entries = agent_type.get_lockfile_entries_mut(&mut lockfile);
625 assert_eq!(entries.len(), 1);
626 assert_eq!(entries[0].name, "test-agent");
627
628 entries.push(LockedResource {
630 name: "new-agent".to_string(),
631 source: Some("test".to_string()),
632 url: Some("https://example.com/repo.git".to_string()),
633 path: "agents/new.md".to_string(),
634 version: Some("v1.0.0".to_string()),
635 resolved_commit: Some("xyz789".to_string()),
636 checksum: "sha256:xyz".to_string(),
637 installed_at: ".claude/agents/new-agent.md".to_string(),
638 dependencies: vec![],
639 resource_type: crate::core::ResourceType::Agent,
640 context_checksum: None,
641 tool: Some("claude-code".to_string()),
642 manifest_alias: None,
643 applied_patches: std::collections::BTreeMap::new(),
644 install: None,
645 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
646 is_private: false,
647 approximate_token_count: None,
648 });
649
650 assert_eq!(lockfile.agents.len(), 2);
652 assert_eq!(lockfile.agents[1].name, "new-agent");
653
654 let mut snippet_type = ResourceType::Snippet;
656 let snippet_entries = snippet_type.get_lockfile_entries_mut(&mut lockfile);
657 assert_eq!(snippet_entries.len(), 1);
658
659 let mut command_type = ResourceType::Command;
660 let command_entries = command_type.get_lockfile_entries_mut(&mut lockfile);
661 assert_eq!(command_entries.len(), 0);
662
663 let mut script_type = ResourceType::Script;
664 let script_entries = script_type.get_lockfile_entries_mut(&mut lockfile);
665 assert_eq!(script_entries.len(), 0);
666
667 let mut hook_type = ResourceType::Hook;
668 let hook_entries = hook_type.get_lockfile_entries_mut(&mut lockfile);
669 assert_eq!(hook_entries.len(), 0);
670
671 let mut mcp_type = ResourceType::McpServer;
672 let mcp_entries = mcp_type.get_lockfile_entries_mut(&mut lockfile);
673 assert_eq!(mcp_entries.len(), 0);
674 }
675
676 #[test]
677 fn test_collect_all_entries() {
678 let lockfile = create_test_lockfile();
679 let manifest = create_test_manifest();
680
681 let entries = ResourceIterator::collect_all_entries(&lockfile, &manifest);
682 assert_eq!(entries.len(), 2);
683
684 assert_eq!(entries[0].0.name, "test-agent");
685 assert_eq!(normalize_path_for_storage(entries[0].1.as_ref()), ".claude/agents/agpm");
687
688 assert_eq!(entries[1].0.name, "test-snippet");
689 assert_eq!(normalize_path_for_storage(entries[1].1.as_ref()), ".claude/snippets/agpm");
692 }
693
694 #[test]
695 fn test_collect_all_entries_empty_lockfile() {
696 let empty_lockfile = LockFile::new();
697 let manifest = create_test_manifest();
698
699 let entries = ResourceIterator::collect_all_entries(&empty_lockfile, &manifest);
700 assert_eq!(entries.len(), 0);
701 }
702
703 #[test]
704 fn test_collect_all_entries_multiple_resources() {
705 let lockfile = create_multi_resource_lockfile();
706 let manifest = create_test_manifest();
707
708 let entries = ResourceIterator::collect_all_entries(&lockfile, &manifest);
709
710 assert_eq!(entries.len(), 5);
713
714 let mut found_types = std::collections::HashSet::new();
716 for (resource, _) in &entries {
717 match resource.name.as_str() {
718 "agent1" | "agent2" => {
719 found_types.insert("agent");
720 }
721 "local-snippet" => {
722 found_types.insert("snippet");
723 }
724 "command1" => {
725 found_types.insert("command");
726 }
727 "script1" => {
728 found_types.insert("script");
729 }
730 "hook1" | "mcp1" => {
732 panic!("Hooks and MCP servers should not be in collected entries");
733 }
734 _ => {}
735 }
736 }
737
738 assert_eq!(found_types.len(), 4);
739 }
740
741 #[test]
742 fn test_find_resource_by_name() {
743 let lockfile = create_test_lockfile();
744
745 let result = ResourceIterator::find_resource_by_name(&lockfile, "test-agent");
746 assert!(result.is_some());
747 let (rt, resource) = result.unwrap();
748 assert_eq!(rt, ResourceType::Agent);
749 assert_eq!(resource.name, "test-agent");
750
751 let result = ResourceIterator::find_resource_by_name(&lockfile, "nonexistent");
752 assert!(result.is_none());
753 }
754
755 #[test]
756 fn test_find_resource_by_name_multiple_types() {
757 let lockfile = create_multi_resource_lockfile();
758
759 let result = ResourceIterator::find_resource_by_name(&lockfile, "agent1");
761 assert!(result.is_some());
762 let (rt, resource) = result.unwrap();
763 assert_eq!(rt, ResourceType::Agent);
764 assert_eq!(resource.name, "agent1");
765
766 let result = ResourceIterator::find_resource_by_name(&lockfile, "command1");
768 assert!(result.is_some());
769 let (rt, resource) = result.unwrap();
770 assert_eq!(rt, ResourceType::Command);
771 assert_eq!(resource.name, "command1");
772
773 let result = ResourceIterator::find_resource_by_name(&lockfile, "script1");
775 assert!(result.is_some());
776 let (rt, resource) = result.unwrap();
777 assert_eq!(rt, ResourceType::Script);
778 assert_eq!(resource.name, "script1");
779
780 let result = ResourceIterator::find_resource_by_name(&lockfile, "hook1");
782 assert!(result.is_some());
783 let (rt, resource) = result.unwrap();
784 assert_eq!(rt, ResourceType::Hook);
785 assert_eq!(resource.name, "hook1");
786
787 let result = ResourceIterator::find_resource_by_name(&lockfile, "mcp1");
789 assert!(result.is_some());
790 let (rt, resource) = result.unwrap();
791 assert_eq!(rt, ResourceType::McpServer);
792 assert_eq!(resource.name, "mcp1");
793
794 let result = ResourceIterator::find_resource_by_name(&lockfile, "local-snippet");
796 assert!(result.is_some());
797 let (rt, resource) = result.unwrap();
798 assert_eq!(rt, ResourceType::Snippet);
799 assert_eq!(resource.name, "local-snippet");
800 assert!(resource.source.is_none());
801 }
802
803 #[test]
804 fn test_count_and_has_resources() {
805 let lockfile = create_test_lockfile();
806 assert_eq!(ResourceIterator::count_total_resources(&lockfile), 2);
807 assert!(ResourceIterator::has_resources(&lockfile));
808
809 let empty_lockfile = LockFile::new();
810 assert_eq!(ResourceIterator::count_total_resources(&empty_lockfile), 0);
811 assert!(!ResourceIterator::has_resources(&empty_lockfile));
812
813 let multi_lockfile = create_multi_resource_lockfile();
814 assert_eq!(ResourceIterator::count_total_resources(&multi_lockfile), 7);
815 assert!(ResourceIterator::has_resources(&multi_lockfile));
816 }
817
818 #[test]
819 fn test_get_all_resource_names() {
820 let lockfile = create_test_lockfile();
821 let names = ResourceIterator::get_all_resource_names(&lockfile);
822
823 assert_eq!(names.len(), 2);
824 assert!(names.contains(&"test-agent".to_string()));
825 assert!(names.contains(&"test-snippet".to_string()));
826 }
827
828 #[test]
829 fn test_get_all_resource_names_empty() {
830 let empty_lockfile = LockFile::new();
831 let names = ResourceIterator::get_all_resource_names(&empty_lockfile);
832 assert_eq!(names.len(), 0);
833 }
834
835 #[test]
836 fn test_get_all_resource_names_multiple() {
837 let lockfile = create_multi_resource_lockfile();
838 let names = ResourceIterator::get_all_resource_names(&lockfile);
839
840 assert_eq!(names.len(), 7);
841 assert!(names.contains(&"agent1".to_string()));
842 assert!(names.contains(&"agent2".to_string()));
843 assert!(names.contains(&"local-snippet".to_string()));
844 assert!(names.contains(&"command1".to_string()));
845 assert!(names.contains(&"script1".to_string()));
846 assert!(names.contains(&"hook1".to_string()));
847 assert!(names.contains(&"mcp1".to_string()));
848 }
849
850 #[test]
851 fn test_get_resources_by_source() {
852 let lockfile = create_multi_resource_lockfile();
853
854 let source1_resources =
856 ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Agent, "source1");
857 assert_eq!(source1_resources.len(), 1);
858 assert_eq!(source1_resources[0].name, "agent1");
859
860 let source1_commands =
861 ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Command, "source1");
862 assert_eq!(source1_commands.len(), 1);
863 assert_eq!(source1_commands[0].name, "command1");
864
865 let source1_scripts =
866 ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Script, "source1");
867 assert_eq!(source1_scripts.len(), 1);
868 assert_eq!(source1_scripts[0].name, "script1");
869
870 let source1_mcps = ResourceIterator::get_resources_by_source(
871 &lockfile,
872 ResourceType::McpServer,
873 "source1",
874 );
875 assert_eq!(source1_mcps.len(), 1);
876 assert_eq!(source1_mcps[0].name, "mcp1");
877
878 let source2_agents =
880 ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Agent, "source2");
881 assert_eq!(source2_agents.len(), 1);
882 assert_eq!(source2_agents[0].name, "agent2");
883
884 let source2_hooks =
885 ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Hook, "source2");
886 assert_eq!(source2_hooks.len(), 1);
887 assert_eq!(source2_hooks[0].name, "hook1");
888
889 let nonexistent = ResourceIterator::get_resources_by_source(
891 &lockfile,
892 ResourceType::Agent,
893 "nonexistent",
894 );
895 assert_eq!(nonexistent.len(), 0);
896
897 let source1_snippets =
899 ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Snippet, "source1");
900 assert_eq!(source1_snippets.len(), 0);
901 }
902
903 #[test]
904 fn test_for_each_resource() {
905 let lockfile = create_multi_resource_lockfile();
906 let mut visited_resources = Vec::new();
907
908 ResourceIterator::for_each_resource(&lockfile, |resource_type, resource| {
909 visited_resources.push((resource_type, resource.name.clone()));
910 });
911
912 assert_eq!(visited_resources.len(), 7);
913
914 let expected_resources = vec![
916 (ResourceType::Agent, "agent1".to_string()),
917 (ResourceType::Agent, "agent2".to_string()),
918 (ResourceType::Snippet, "local-snippet".to_string()),
919 (ResourceType::Command, "command1".to_string()),
920 (ResourceType::Script, "script1".to_string()),
921 (ResourceType::Hook, "hook1".to_string()),
922 (ResourceType::McpServer, "mcp1".to_string()),
923 ];
924
925 for expected in expected_resources {
926 assert!(visited_resources.contains(&expected));
927 }
928 }
929
930 #[test]
931 fn test_for_each_resource_empty() {
932 let empty_lockfile = LockFile::new();
933 let mut count = 0;
934
935 ResourceIterator::for_each_resource(&empty_lockfile, |_, _| {
936 count += 1;
937 });
938
939 assert_eq!(count, 0);
940 }
941
942 #[test]
943 fn test_map_resources() {
944 let lockfile = create_multi_resource_lockfile();
945
946 let names = ResourceIterator::map_resources(&lockfile, |_, resource| resource.name.clone());
948
949 assert_eq!(names.len(), 7);
950 assert!(names.contains(&"agent1".to_string()));
951 assert!(names.contains(&"agent2".to_string()));
952 assert!(names.contains(&"local-snippet".to_string()));
953 assert!(names.contains(&"command1".to_string()));
954 assert!(names.contains(&"script1".to_string()));
955 assert!(names.contains(&"hook1".to_string()));
956 assert!(names.contains(&"mcp1".to_string()));
957
958 let type_name_pairs =
960 ResourceIterator::map_resources(&lockfile, |resource_type, resource| {
961 format!("{}:{}", resource_type, resource.name)
962 });
963
964 assert_eq!(type_name_pairs.len(), 7);
965 assert!(type_name_pairs.contains(&"agent:agent1".to_string()));
966 assert!(type_name_pairs.contains(&"agent:agent2".to_string()));
967 assert!(type_name_pairs.contains(&"snippet:local-snippet".to_string()));
968 assert!(type_name_pairs.contains(&"command:command1".to_string()));
969 assert!(type_name_pairs.contains(&"script:script1".to_string()));
970 assert!(type_name_pairs.contains(&"hook:hook1".to_string()));
971 assert!(type_name_pairs.contains(&"mcp-server:mcp1".to_string()));
972 }
973
974 #[test]
975 fn test_map_resources_empty() {
976 let empty_lockfile = LockFile::new();
977
978 let results =
979 ResourceIterator::map_resources(&empty_lockfile, |_, resource| resource.name.clone());
980
981 assert_eq!(results.len(), 0);
982 }
983
984 #[test]
985 fn test_filter_resources() {
986 let lockfile = create_multi_resource_lockfile();
987
988 let source1_resources = ResourceIterator::filter_resources(&lockfile, |_, resource| {
990 resource.source.as_deref() == Some("source1")
991 });
992
993 assert_eq!(source1_resources.len(), 4); let source1_names: Vec<String> =
995 source1_resources.iter().map(|(_, r)| r.name.clone()).collect();
996 assert!(source1_names.contains(&"agent1".to_string()));
997 assert!(source1_names.contains(&"command1".to_string()));
998 assert!(source1_names.contains(&"script1".to_string()));
999 assert!(source1_names.contains(&"mcp1".to_string()));
1000
1001 let agents = ResourceIterator::filter_resources(&lockfile, |resource_type, _| {
1003 resource_type == ResourceType::Agent
1004 });
1005
1006 assert_eq!(agents.len(), 2); let agent_names: Vec<String> = agents.iter().map(|(_, r)| r.name.clone()).collect();
1008 assert!(agent_names.contains(&"agent1".to_string()));
1009 assert!(agent_names.contains(&"agent2".to_string()));
1010
1011 let no_source_resources =
1013 ResourceIterator::filter_resources(&lockfile, |_, resource| resource.source.is_none());
1014
1015 assert_eq!(no_source_resources.len(), 1); assert_eq!(no_source_resources[0].1.name, "local-snippet");
1017
1018 let v1_resources = ResourceIterator::filter_resources(&lockfile, |_, resource| {
1020 resource.version.as_deref().unwrap_or("").starts_with("v1.")
1021 });
1022
1023 assert_eq!(v1_resources.len(), 5); let no_matches = ResourceIterator::filter_resources(&lockfile, |_, resource| {
1027 resource.name == "nonexistent"
1028 });
1029
1030 assert_eq!(no_matches.len(), 0);
1031 }
1032
1033 #[test]
1034 fn test_filter_resources_empty() {
1035 let empty_lockfile = LockFile::new();
1036
1037 let results = ResourceIterator::filter_resources(&empty_lockfile, |_, _| true);
1038 assert_eq!(results.len(), 0);
1039 }
1040
1041 #[test]
1042 fn test_group_by_source() {
1043 let lockfile = create_multi_resource_lockfile();
1044
1045 let groups = ResourceIterator::group_by_source(&lockfile);
1046
1047 assert_eq!(groups.len(), 2); let source1_group = groups.get("source1").unwrap();
1051 assert_eq!(source1_group.len(), 4); let source1_names: Vec<String> =
1054 source1_group.iter().map(|(_, r)| r.name.clone()).collect();
1055 assert!(source1_names.contains(&"agent1".to_string()));
1056 assert!(source1_names.contains(&"command1".to_string()));
1057 assert!(source1_names.contains(&"script1".to_string()));
1058 assert!(source1_names.contains(&"mcp1".to_string()));
1059
1060 let source2_group = groups.get("source2").unwrap();
1062 assert_eq!(source2_group.len(), 2); let source2_names: Vec<String> =
1065 source2_group.iter().map(|(_, r)| r.name.clone()).collect();
1066 assert!(source2_names.contains(&"agent2".to_string()));
1067 assert!(source2_names.contains(&"hook1".to_string()));
1068
1069 assert!(!groups.contains_key(""));
1071 }
1072
1073 #[test]
1074 fn test_group_by_source_empty() {
1075 let empty_lockfile = LockFile::new();
1076
1077 let groups = ResourceIterator::group_by_source(&empty_lockfile);
1078 assert_eq!(groups.len(), 0);
1079 }
1080
1081 #[test]
1082 fn test_group_by_source_no_sources() {
1083 let mut lockfile = LockFile::new();
1084
1085 lockfile.agents.push(LockedResource {
1087 name: "local-agent".to_string(),
1088 source: None,
1089 url: None,
1090 path: "local/agent.md".to_string(),
1091 version: None,
1092 resolved_commit: None,
1093 checksum: "sha256:local".to_string(),
1094 installed_at: ".claude/agents/local-agent.md".to_string(),
1095 dependencies: vec![],
1096 resource_type: crate::core::ResourceType::Agent,
1097 context_checksum: None,
1098 tool: Some("claude-code".to_string()),
1099 manifest_alias: None,
1100 applied_patches: std::collections::BTreeMap::new(),
1101 install: None,
1102 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
1103 is_private: false,
1104 approximate_token_count: None,
1105 });
1106
1107 let groups = ResourceIterator::group_by_source(&lockfile);
1108 assert_eq!(groups.len(), 0); }
1110
1111 #[test]
1112 fn test_resource_type_ext() {
1113 let lockfile = create_test_lockfile();
1114
1115 assert_eq!(ResourceType::Agent.get_lockfile_entries(&lockfile).len(), 1);
1116 assert_eq!(ResourceType::Snippet.get_lockfile_entries(&lockfile).len(), 1);
1117 assert_eq!(ResourceType::Command.get_lockfile_entries(&lockfile).len(), 0);
1118 }
1119
1120 #[test]
1121 fn test_resource_type_ext_all_types() {
1122 let lockfile = create_multi_resource_lockfile();
1123
1124 assert_eq!(ResourceType::Agent.get_lockfile_entries(&lockfile).len(), 2);
1125 assert_eq!(ResourceType::Snippet.get_lockfile_entries(&lockfile).len(), 1);
1126 assert_eq!(ResourceType::Command.get_lockfile_entries(&lockfile).len(), 1);
1127 assert_eq!(ResourceType::Script.get_lockfile_entries(&lockfile).len(), 1);
1128 assert_eq!(ResourceType::Hook.get_lockfile_entries(&lockfile).len(), 1);
1129 assert_eq!(ResourceType::McpServer.get_lockfile_entries(&lockfile).len(), 1);
1130 }
1131}