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