1use std::collections::{HashMap, HashSet};
4
5use crate::file_types::FileType;
6use crate::rules::Validator;
7
8pub type ValidatorFactory = fn() -> Box<dyn Validator>;
10
11pub trait ValidatorProvider: Send + Sync {
37 fn name(&self) -> &str {
41 let full = std::any::type_name::<Self>();
42 full.rsplit("::").next().unwrap_or(full)
43 }
44
45 fn validators(&self) -> Vec<(FileType, ValidatorFactory)>;
47
48 fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
74 self.validators()
75 .into_iter()
76 .map(|(ft, f)| (ft, None, f))
77 .collect()
78 }
79}
80
81pub(crate) struct BuiltinProvider;
87
88impl ValidatorProvider for BuiltinProvider {
89 fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
90 self.named_validators()
91 .into_iter()
92 .map(|(ft, _, f)| (ft, f))
93 .collect()
94 }
95
96 fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
97 let providers: &[&dyn ValidatorProvider] = &[
98 &SkillProvider,
99 &ClaudeProvider,
100 &CopilotProvider,
101 &CursorProvider,
102 &GeminiProvider,
103 &RooProvider,
104 &WindsurfProvider,
105 &MiscProvider,
106 ];
107 let result: Vec<_> = providers
108 .iter()
109 .flat_map(|p| p.named_validators())
110 .collect();
111 debug_assert_eq!(
112 result.len(),
113 EXPECTED_BUILTIN_COUNT,
114 "BuiltinProvider produced {} entries but expected {}",
115 result.len(),
116 EXPECTED_BUILTIN_COUNT
117 );
118 result
119 }
120}
121
122pub struct ValidatorRegistry {
132 validators: HashMap<FileType, Vec<Box<dyn Validator>>>,
136 disabled_validators: HashSet<String>,
137}
138
139impl ValidatorRegistry {
140 pub fn new() -> Self {
142 Self {
143 validators: HashMap::new(),
144 disabled_validators: HashSet::new(),
145 }
146 }
147
148 pub fn with_defaults() -> Self {
150 let mut registry = Self::new();
151 registry.register_defaults();
152 registry
153 }
154
155 pub fn builder() -> ValidatorRegistryBuilder {
168 ValidatorRegistryBuilder::new()
169 }
170
171 pub fn register(&mut self, file_type: FileType, factory: ValidatorFactory) {
180 let instance = factory();
181 if self.disabled_validators.contains(instance.name() as &str) {
182 return;
183 }
184 self.validators.entry(file_type).or_default().push(instance);
185 }
186
187 fn register_named(&mut self, file_type: FileType, name: &str, factory: ValidatorFactory) {
201 if self.disabled_validators.contains(name) {
202 return;
203 }
204 let instance = factory();
205 #[cfg(debug_assertions)]
206 {
207 let runtime_name = instance.name();
208 assert_eq!(
209 name, runtime_name,
210 "ValidatorProvider name/factory mismatch: static name \"{name}\" \
211 does not match factory().name() \"{runtime_name}\". The static name \
212 passed to named_validators() must equal the value returned by \
213 Validator::name().",
214 );
215 }
216 self.validators.entry(file_type).or_default().push(instance);
217 }
218
219 pub fn total_validator_count(&self) -> usize {
221 self.validators.values().map(|v| v.len()).sum()
222 }
223
224 #[deprecated(
226 since = "0.12.2",
227 note = "renamed to total_validator_count() - validators are now cached, not re-instantiated"
228 )]
229 pub fn total_factory_count(&self) -> usize {
230 self.total_validator_count()
231 }
232
233 pub fn validators_for(&self, file_type: FileType) -> &[Box<dyn Validator>] {
239 match self.validators.get(&file_type) {
240 Some(v) => v,
241 None => &[],
242 }
243 }
244
245 pub fn disable_validator(&mut self, name: &'static str) {
252 if self.disabled_validators.insert(name.to_string()) {
253 self.remove_disabled_from_cache(name);
254 }
255 }
256
257 pub fn disable_validator_owned(&mut self, name: &str) {
262 if self.disabled_validators.insert(name.to_string()) {
263 self.remove_disabled_from_cache(name);
264 }
265 }
266
267 pub fn disabled_validator_count(&self) -> usize {
269 self.disabled_validators.len()
270 }
271
272 fn remove_disabled_from_cache(&mut self, name: &str) {
274 for instances in self.validators.values_mut() {
275 instances.retain(|v| v.name() != name);
276 }
277 }
278
279 fn register_defaults(&mut self) {
280 for (file_type, name, factory) in BuiltinProvider.named_validators() {
281 match name {
282 Some(n) => self.register_named(file_type, n, factory),
283 None => self.register(file_type, factory),
284 }
285 }
286 }
287}
288
289impl Default for ValidatorRegistry {
290 fn default() -> Self {
291 Self::with_defaults()
292 }
293}
294
295pub struct ValidatorRegistryBuilder {
315 entries: Vec<(FileType, Option<&'static str>, ValidatorFactory)>,
316 disabled_validators: HashSet<String>,
317}
318
319impl ValidatorRegistryBuilder {
320 fn new() -> Self {
322 Self {
323 entries: Vec::new(),
324 disabled_validators: HashSet::new(),
325 }
326 }
327
328 pub fn with_defaults(&mut self) -> &mut Self {
333 self.with_provider(&BuiltinProvider)
334 }
335
336 pub fn with_provider(&mut self, provider: &dyn ValidatorProvider) -> &mut Self {
338 self.entries.extend(provider.named_validators());
339 self
340 }
341
342 pub fn register(&mut self, file_type: FileType, factory: ValidatorFactory) -> &mut Self {
344 self.entries.push((file_type, None, factory));
345 self
346 }
347
348 pub fn without_validator(&mut self, name: &'static str) -> &mut Self {
353 self.disabled_validators.insert(name.to_string());
354 self
355 }
356
357 pub fn without_validator_owned(&mut self, name: &str) -> &mut Self {
362 self.disabled_validators.insert(name.to_string());
363 self
364 }
365
366 pub fn build(&mut self) -> ValidatorRegistry {
379 let mut registry = ValidatorRegistry {
380 validators: HashMap::new(),
381 disabled_validators: std::mem::take(&mut self.disabled_validators),
382 };
383 for &(file_type, name, factory) in &self.entries {
384 match name {
385 Some(n) => registry.register_named(file_type, n, factory),
386 None => registry.register(file_type, factory),
387 }
388 }
389 registry
390 }
391}
392
393const EXPECTED_BUILTIN_COUNT: usize = 71;
402
403struct SkillProvider;
412
413impl ValidatorProvider for SkillProvider {
414 fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
415 self.named_validators()
416 .into_iter()
417 .map(|(ft, _, f)| (ft, f))
418 .collect()
419 }
420
421 fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
422 vec![
423 (FileType::Skill, Some("SkillValidator"), skill_validator),
424 (
425 FileType::Skill,
426 Some("PerClientSkillValidator"),
427 per_client_skill_validator,
428 ),
429 (FileType::Skill, Some("XmlValidator"), xml_validator),
430 (FileType::Skill, Some("ImportsValidator"), imports_validator),
431 ]
432 }
433}
434
435struct ClaudeProvider;
437
438impl ValidatorProvider for ClaudeProvider {
439 fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
440 self.named_validators()
441 .into_iter()
442 .map(|(ft, _, f)| (ft, f))
443 .collect()
444 }
445
446 fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
447 vec![
448 (
449 FileType::ClaudeMd,
450 Some("ClaudeMdValidator"),
451 claude_md_validator,
452 ),
453 (
454 FileType::ClaudeMd,
455 Some("CrossPlatformValidator"),
456 cross_platform_validator,
457 ),
458 (
459 FileType::ClaudeMd,
460 Some("AgentsMdValidator"),
461 agents_md_validator,
462 ),
463 (FileType::ClaudeMd, Some("AmpValidator"), amp_validator),
464 (FileType::ClaudeMd, Some("XmlValidator"), xml_validator),
465 (
466 FileType::ClaudeMd,
467 Some("ImportsValidator"),
468 imports_validator,
469 ),
470 (
471 FileType::ClaudeMd,
472 Some("PromptValidator"),
473 prompt_validator,
474 ),
475 (FileType::ClaudeMd, Some("CodexValidator"), codex_validator),
478 (FileType::Agent, Some("AgentValidator"), agent_validator),
479 (FileType::Agent, Some("XmlValidator"), xml_validator),
480 (
481 FileType::ClaudeRule,
482 Some("ClaudeRulesValidator"),
483 claude_rules_validator,
484 ),
485 ]
486 }
487}
488
489struct CopilotProvider;
491
492impl ValidatorProvider for CopilotProvider {
493 fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
494 self.named_validators()
495 .into_iter()
496 .map(|(ft, _, f)| (ft, f))
497 .collect()
498 }
499
500 fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
501 vec![
502 (
503 FileType::Copilot,
504 Some("CopilotValidator"),
505 copilot_validator,
506 ),
507 (FileType::Copilot, Some("XmlValidator"), xml_validator),
508 (
509 FileType::CopilotScoped,
510 Some("CopilotValidator"),
511 copilot_validator,
512 ),
513 (FileType::CopilotScoped, Some("XmlValidator"), xml_validator),
514 (
515 FileType::CopilotAgent,
516 Some("CopilotValidator"),
517 copilot_validator,
518 ),
519 (FileType::CopilotAgent, Some("XmlValidator"), xml_validator),
520 (
521 FileType::CopilotPrompt,
522 Some("CopilotValidator"),
523 copilot_validator,
524 ),
525 (FileType::CopilotPrompt, Some("XmlValidator"), xml_validator),
526 (
527 FileType::CopilotHooks,
528 Some("CopilotValidator"),
529 copilot_validator,
530 ),
531 ]
532 }
533}
534
535struct CursorProvider;
537
538impl ValidatorProvider for CursorProvider {
539 fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
540 self.named_validators()
541 .into_iter()
542 .map(|(ft, _, f)| (ft, f))
543 .collect()
544 }
545
546 fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
547 vec![
548 (
549 FileType::CursorRule,
550 Some("CursorValidator"),
551 cursor_validator,
552 ),
553 (
554 FileType::CursorRule,
555 Some("PromptValidator"),
556 prompt_validator,
557 ),
558 (
559 FileType::CursorRule,
560 Some("ClaudeMdValidator"),
561 claude_md_validator,
562 ),
563 (
564 FileType::CursorHooks,
565 Some("CursorValidator"),
566 cursor_validator,
567 ),
568 (
569 FileType::CursorAgent,
570 Some("CursorValidator"),
571 cursor_validator,
572 ),
573 (
574 FileType::CursorEnvironment,
575 Some("CursorValidator"),
576 cursor_validator,
577 ),
578 (
579 FileType::CursorRulesLegacy,
580 Some("CursorValidator"),
581 cursor_validator,
582 ),
583 (
584 FileType::CursorRulesLegacy,
585 Some("PromptValidator"),
586 prompt_validator,
587 ),
588 (
589 FileType::CursorRulesLegacy,
590 Some("ClaudeMdValidator"),
591 claude_md_validator,
592 ),
593 ]
594 }
595}
596
597struct GeminiProvider;
599
600impl ValidatorProvider for GeminiProvider {
601 fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
602 self.named_validators()
603 .into_iter()
604 .map(|(ft, _, f)| (ft, f))
605 .collect()
606 }
607
608 fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
609 vec![
610 (
611 FileType::GeminiMd,
612 Some("GeminiMdValidator"),
613 gemini_md_validator,
614 ),
615 (
616 FileType::GeminiMd,
617 Some("PromptValidator"),
618 prompt_validator,
619 ),
620 (FileType::GeminiMd, Some("XmlValidator"), xml_validator),
621 (
622 FileType::GeminiMd,
623 Some("ImportsValidator"),
624 imports_validator,
625 ),
626 (
627 FileType::GeminiMd,
628 Some("CrossPlatformValidator"),
629 cross_platform_validator,
630 ),
631 (
632 FileType::GeminiSettings,
633 Some("GeminiSettingsValidator"),
634 gemini_settings_validator,
635 ),
636 (
637 FileType::GeminiExtension,
638 Some("GeminiExtensionValidator"),
639 gemini_extension_validator,
640 ),
641 (
642 FileType::GeminiIgnore,
643 Some("GeminiIgnoreValidator"),
644 gemini_ignore_validator,
645 ),
646 ]
647 }
648}
649
650struct RooProvider;
652
653impl ValidatorProvider for RooProvider {
654 fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
655 self.named_validators()
656 .into_iter()
657 .map(|(ft, _, f)| (ft, f))
658 .collect()
659 }
660
661 fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
662 vec![
663 (FileType::RooRules, Some("RooCodeValidator"), roo_validator),
664 (FileType::RooModes, Some("RooCodeValidator"), roo_validator),
665 (FileType::RooIgnore, Some("RooCodeValidator"), roo_validator),
666 (
667 FileType::RooModeRules,
668 Some("RooCodeValidator"),
669 roo_validator,
670 ),
671 (FileType::RooMcp, Some("RooCodeValidator"), roo_validator),
672 ]
673 }
674}
675
676struct WindsurfProvider;
678
679impl ValidatorProvider for WindsurfProvider {
680 fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
681 self.named_validators()
682 .into_iter()
683 .map(|(ft, _, f)| (ft, f))
684 .collect()
685 }
686
687 fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
688 vec![
689 (
690 FileType::WindsurfRule,
691 Some("WindsurfValidator"),
692 windsurf_validator,
693 ),
694 (
695 FileType::WindsurfWorkflow,
696 Some("WindsurfValidator"),
697 windsurf_validator,
698 ),
699 (
700 FileType::WindsurfRulesLegacy,
701 Some("WindsurfValidator"),
702 windsurf_validator,
703 ),
704 ]
705 }
706}
707
708struct MiscProvider;
710
711impl ValidatorProvider for MiscProvider {
712 fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
713 self.named_validators()
714 .into_iter()
715 .map(|(ft, _, f)| (ft, f))
716 .collect()
717 }
718
719 fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
720 vec![
721 (FileType::AmpCheck, Some("AmpValidator"), amp_validator),
722 (FileType::Hooks, Some("HooksValidator"), hooks_validator),
723 (FileType::Plugin, Some("PluginValidator"), plugin_validator),
724 (FileType::Mcp, Some("McpValidator"), mcp_validator),
725 (
726 FileType::ClineRules,
727 Some("ClineValidator"),
728 cline_validator,
729 ),
730 (
731 FileType::ClineRulesFolder,
732 Some("ClineValidator"),
733 cline_validator,
734 ),
735 (
736 FileType::OpenCodeConfig,
737 Some("OpenCodeValidator"),
738 opencode_validator,
739 ),
740 (FileType::AmpSettings, Some("AmpValidator"), amp_validator),
741 (
742 FileType::CodexConfig,
743 Some("CodexConfigValidator"),
744 codex_config_validator,
745 ),
746 (
747 FileType::KiroSteering,
748 Some("KiroSteeringValidator"),
749 kiro_steering_validator,
750 ),
751 (
752 FileType::KiroPower,
753 Some("KiroPowerValidator"),
754 kiro_power_validator,
755 ),
756 (
757 FileType::KiroPower,
758 Some("ImportsValidator"),
759 imports_validator,
760 ),
761 (
762 FileType::KiroPower,
763 Some("CrossPlatformValidator"),
764 cross_platform_validator,
765 ),
766 (FileType::KiroPower, Some("XmlValidator"), xml_validator),
767 (
768 FileType::KiroAgent,
769 Some("KiroAgentValidator"),
770 kiro_agent_validator,
771 ),
772 (
773 FileType::KiroAgent,
774 Some("ImportsValidator"),
775 imports_validator,
776 ),
777 (
778 FileType::KiroHook,
779 Some("KiroHookValidator"),
780 kiro_hook_validator,
781 ),
782 (
783 FileType::KiroHook,
784 Some("ImportsValidator"),
785 imports_validator,
786 ),
787 (
788 FileType::KiroMcp,
789 Some("KiroMcpValidator"),
790 kiro_mcp_validator,
791 ),
792 (
793 FileType::GenericMarkdown,
794 Some("CrossPlatformValidator"),
795 cross_platform_validator,
796 ),
797 (
798 FileType::GenericMarkdown,
799 Some("XmlValidator"),
800 xml_validator,
801 ),
802 (
803 FileType::GenericMarkdown,
804 Some("ImportsValidator"),
805 imports_validator,
806 ),
807 ]
808 }
809}
810
811fn skill_validator() -> Box<dyn Validator> {
816 Box::new(crate::rules::skill::SkillValidator)
817}
818
819fn per_client_skill_validator() -> Box<dyn Validator> {
820 Box::new(crate::rules::per_client_skill::PerClientSkillValidator)
821}
822
823fn amp_validator() -> Box<dyn Validator> {
824 Box::new(crate::rules::amp::AmpValidator)
825}
826
827fn claude_md_validator() -> Box<dyn Validator> {
828 Box::new(crate::rules::claude_md::ClaudeMdValidator)
829}
830
831fn agents_md_validator() -> Box<dyn Validator> {
832 Box::new(crate::rules::agents_md::AgentsMdValidator)
833}
834
835fn agent_validator() -> Box<dyn Validator> {
836 Box::new(crate::rules::agent::AgentValidator)
837}
838
839fn hooks_validator() -> Box<dyn Validator> {
840 Box::new(crate::rules::hooks::HooksValidator)
841}
842
843fn plugin_validator() -> Box<dyn Validator> {
844 Box::new(crate::rules::plugin::PluginValidator)
845}
846
847fn mcp_validator() -> Box<dyn Validator> {
848 Box::new(crate::rules::mcp::McpValidator)
849}
850
851fn xml_validator() -> Box<dyn Validator> {
852 Box::new(crate::rules::xml::XmlValidator)
853}
854
855fn imports_validator() -> Box<dyn Validator> {
856 Box::new(crate::rules::imports::ImportsValidator)
857}
858
859fn cross_platform_validator() -> Box<dyn Validator> {
860 Box::new(crate::rules::cross_platform::CrossPlatformValidator)
861}
862
863fn prompt_validator() -> Box<dyn Validator> {
864 Box::new(crate::rules::prompt::PromptValidator)
865}
866
867fn copilot_validator() -> Box<dyn Validator> {
868 Box::new(crate::rules::copilot::CopilotValidator)
869}
870
871fn claude_rules_validator() -> Box<dyn Validator> {
872 Box::new(crate::rules::claude_rules::ClaudeRulesValidator)
873}
874
875fn cursor_validator() -> Box<dyn Validator> {
876 Box::new(crate::rules::cursor::CursorValidator)
877}
878
879fn cline_validator() -> Box<dyn Validator> {
880 Box::new(crate::rules::cline::ClineValidator)
881}
882
883fn opencode_validator() -> Box<dyn Validator> {
884 Box::new(crate::rules::opencode::OpenCodeValidator)
885}
886
887fn gemini_md_validator() -> Box<dyn Validator> {
888 Box::new(crate::rules::gemini_md::GeminiMdValidator)
889}
890
891fn gemini_settings_validator() -> Box<dyn Validator> {
892 Box::new(crate::rules::gemini_settings::GeminiSettingsValidator)
893}
894
895fn gemini_extension_validator() -> Box<dyn Validator> {
896 Box::new(crate::rules::gemini_extension::GeminiExtensionValidator)
897}
898
899fn gemini_ignore_validator() -> Box<dyn Validator> {
900 Box::new(crate::rules::gemini_ignore::GeminiIgnoreValidator)
901}
902
903fn codex_validator() -> Box<dyn Validator> {
904 Box::new(crate::rules::codex::CodexValidator)
905}
906
907fn codex_config_validator() -> Box<dyn Validator> {
908 Box::new(crate::rules::codex::CodexConfigValidator)
909}
910
911fn roo_validator() -> Box<dyn Validator> {
912 Box::new(crate::rules::roo::RooCodeValidator)
913}
914
915fn windsurf_validator() -> Box<dyn Validator> {
916 Box::new(crate::rules::windsurf::WindsurfValidator)
917}
918
919fn kiro_steering_validator() -> Box<dyn Validator> {
920 Box::new(crate::rules::kiro_steering::KiroSteeringValidator)
921}
922
923fn kiro_agent_validator() -> Box<dyn Validator> {
924 Box::new(crate::rules::kiro_agent::KiroAgentValidator)
925}
926
927fn kiro_power_validator() -> Box<dyn Validator> {
928 Box::new(crate::rules::kiro_power::KiroPowerValidator)
929}
930
931fn kiro_hook_validator() -> Box<dyn Validator> {
932 Box::new(crate::rules::kiro_hook::KiroHookValidator)
933}
934
935fn kiro_mcp_validator() -> Box<dyn Validator> {
936 Box::new(crate::rules::kiro_mcp::KiroMcpValidator)
937}
938
939#[cfg(test)]
940mod tests {
941 use super::*;
942 use std::sync::atomic::{AtomicUsize, Ordering};
943
944 #[test]
947 fn builtin_provider_returns_expected_count() {
948 let provider = BuiltinProvider;
949 let entries = provider.validators();
950 assert_eq!(
951 entries.len(),
952 EXPECTED_BUILTIN_COUNT,
953 "BuiltinProvider should return the same number of entries as EXPECTED_BUILTIN_COUNT"
954 );
955 }
956
957 #[test]
958 fn builtin_provider_name() {
959 let provider = BuiltinProvider;
960 assert_eq!(provider.name(), "BuiltinProvider");
961 }
962
963 #[test]
966 fn builder_with_defaults_matches_with_defaults() {
967 let via_builder = ValidatorRegistry::builder().with_defaults().build();
968 let via_direct = ValidatorRegistry::with_defaults();
969
970 assert_eq!(
971 via_builder.total_validator_count(),
972 via_direct.total_validator_count(),
973 "Builder with_defaults should produce the same validator count as with_defaults()"
974 );
975 }
976
977 #[test]
978 fn builder_empty_produces_empty_registry() {
979 let registry = ValidatorRegistry::builder().build();
980 assert_eq!(registry.total_validator_count(), 0);
981 }
982
983 #[test]
984 fn builder_register_adds_single_factory() {
985 let registry = ValidatorRegistry::builder()
986 .register(FileType::Skill, skill_validator)
987 .build();
988
989 assert_eq!(registry.total_validator_count(), 1);
990 let validators = registry.validators_for(FileType::Skill);
991 assert_eq!(validators.len(), 1);
992 assert_eq!(validators[0].name(), "SkillValidator");
993 }
994
995 #[test]
996 fn builder_without_validator_disables() {
997 let registry = ValidatorRegistry::builder()
998 .with_defaults()
999 .without_validator("XmlValidator")
1000 .build();
1001
1002 let skill_validators = registry.validators_for(FileType::Skill);
1004 let names: Vec<&str> = skill_validators.iter().map(|v| v.name()).collect();
1005 assert!(
1006 !names.contains(&"XmlValidator"),
1007 "XmlValidator should be disabled, got: {:?}",
1008 names
1009 );
1010
1011 assert!(
1013 names.contains(&"SkillValidator"),
1014 "SkillValidator should still be present, got: {:?}",
1015 names
1016 );
1017 }
1018
1019 struct TestProvider;
1022 impl ValidatorProvider for TestProvider {
1023 fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
1024 vec![(FileType::Skill, skill_validator)]
1025 }
1026 }
1027
1028 #[test]
1029 fn custom_provider_adds_validators() {
1030 let registry = ValidatorRegistry::builder()
1031 .with_provider(&TestProvider)
1032 .build();
1033
1034 assert_eq!(registry.total_validator_count(), 1);
1035 let validators = registry.validators_for(FileType::Skill);
1036 assert_eq!(validators.len(), 1);
1037 }
1038
1039 #[test]
1040 fn custom_provider_name() {
1041 let provider = TestProvider;
1042 assert_eq!(provider.name(), "TestProvider");
1043 }
1044
1045 #[test]
1048 fn disable_validator_filters_from_results() {
1049 let mut registry = ValidatorRegistry::with_defaults();
1050 assert_eq!(registry.disabled_validator_count(), 0);
1051
1052 registry.disable_validator("XmlValidator");
1053 assert_eq!(registry.disabled_validator_count(), 1);
1054
1055 let skill_validators = registry.validators_for(FileType::Skill);
1056 let names: Vec<&str> = skill_validators.iter().map(|v| v.name()).collect();
1057 assert!(!names.contains(&"XmlValidator"));
1058 }
1059
1060 static SKIP_COUNTING_CONSTRUCTED: AtomicUsize = AtomicUsize::new(0);
1064
1065 struct SkipCountingValidator;
1066
1067 impl Validator for SkipCountingValidator {
1068 fn validate(
1069 &self,
1070 _path: &std::path::Path,
1071 _content: &str,
1072 _config: &crate::config::LintConfig,
1073 ) -> Vec<crate::diagnostics::Diagnostic> {
1074 Vec::new()
1075 }
1076
1077 fn name(&self) -> &'static str {
1078 "SkipCountingValidator"
1079 }
1080 }
1081
1082 fn skip_counting_validator_factory() -> Box<dyn Validator> {
1083 SKIP_COUNTING_CONSTRUCTED.fetch_add(1, Ordering::SeqCst);
1084 Box::new(SkipCountingValidator)
1085 }
1086
1087 static ONCE_COUNTING_CONSTRUCTED: AtomicUsize = AtomicUsize::new(0);
1089
1090 struct OnceCountingValidator;
1091
1092 impl Validator for OnceCountingValidator {
1093 fn validate(
1094 &self,
1095 _path: &std::path::Path,
1096 _content: &str,
1097 _config: &crate::config::LintConfig,
1098 ) -> Vec<crate::diagnostics::Diagnostic> {
1099 Vec::new()
1100 }
1101
1102 fn name(&self) -> &'static str {
1103 "OnceCountingValidator"
1104 }
1105 }
1106
1107 fn once_counting_validator_factory() -> Box<dyn Validator> {
1108 ONCE_COUNTING_CONSTRUCTED.fetch_add(1, Ordering::SeqCst);
1109 Box::new(OnceCountingValidator)
1110 }
1111
1112 static BUILDER_COUNTING_CONSTRUCTED: AtomicUsize = AtomicUsize::new(0);
1114
1115 struct BuilderCountingValidator;
1116
1117 impl Validator for BuilderCountingValidator {
1118 fn validate(
1119 &self,
1120 _path: &std::path::Path,
1121 _content: &str,
1122 _config: &crate::config::LintConfig,
1123 ) -> Vec<crate::diagnostics::Diagnostic> {
1124 Vec::new()
1125 }
1126
1127 fn name(&self) -> &'static str {
1128 "BuilderCountingValidator"
1129 }
1130 }
1131
1132 fn builder_counting_validator_factory() -> Box<dyn Validator> {
1133 BUILDER_COUNTING_CONSTRUCTED.fetch_add(1, Ordering::SeqCst);
1134 Box::new(BuilderCountingValidator)
1135 }
1136
1137 #[test]
1138 fn register_skips_disabled_validators() {
1139 SKIP_COUNTING_CONSTRUCTED.store(0, Ordering::SeqCst);
1140
1141 let registry = ValidatorRegistry::builder()
1142 .register(FileType::Skill, skip_counting_validator_factory)
1143 .without_validator("SkipCountingValidator")
1144 .build();
1145
1146 assert_eq!(SKIP_COUNTING_CONSTRUCTED.load(Ordering::SeqCst), 1);
1152
1153 let validators = registry.validators_for(FileType::Skill);
1155 assert!(validators.is_empty());
1156
1157 assert_eq!(SKIP_COUNTING_CONSTRUCTED.load(Ordering::SeqCst), 1);
1159 }
1160
1161 #[test]
1162 fn disable_nonexistent_validator_is_harmless() {
1163 let mut registry = ValidatorRegistry::with_defaults();
1164 registry.disable_validator("NonExistentValidator");
1165 assert_eq!(registry.disabled_validator_count(), 1);
1166
1167 let count_before = ValidatorRegistry::with_defaults().total_validator_count();
1169 assert_eq!(registry.total_validator_count(), count_before);
1170 }
1171
1172 #[test]
1175 fn validators_for_returns_all_when_none_disabled() {
1176 let registry = ValidatorRegistry::with_defaults();
1177 let skill_validators = registry.validators_for(FileType::Skill);
1178 assert_eq!(skill_validators.len(), 4);
1180 }
1181
1182 #[test]
1183 fn validators_for_unknown_file_type_returns_empty() {
1184 let registry = ValidatorRegistry::with_defaults();
1185 let validators = registry.validators_for(FileType::Unknown);
1186 assert!(validators.is_empty());
1187 }
1188
1189 #[test]
1192 fn builder_multiple_without_validators() {
1193 let registry = ValidatorRegistry::builder()
1194 .with_defaults()
1195 .without_validator("XmlValidator")
1196 .without_validator("PromptValidator")
1197 .build();
1198
1199 assert_eq!(registry.disabled_validator_count(), 2);
1200
1201 let skill_names: Vec<&str> = registry
1202 .validators_for(FileType::Skill)
1203 .iter()
1204 .map(|v| v.name())
1205 .collect();
1206 assert!(!skill_names.contains(&"XmlValidator"));
1207
1208 let claude_names: Vec<&str> = registry
1209 .validators_for(FileType::ClaudeMd)
1210 .iter()
1211 .map(|v| v.name())
1212 .collect();
1213 assert!(!claude_names.contains(&"PromptValidator"));
1214 assert!(!claude_names.contains(&"XmlValidator"));
1215 }
1216
1217 #[test]
1218 fn disable_all_validators_for_file_type() {
1219 let registry = ValidatorRegistry::builder()
1220 .with_defaults()
1221 .without_validator("SkillValidator")
1222 .without_validator("PerClientSkillValidator")
1223 .without_validator("XmlValidator")
1224 .without_validator("ImportsValidator")
1225 .build();
1226
1227 assert!(
1228 registry.validators_for(FileType::Skill).is_empty(),
1229 "All Skill validators disabled, should return empty"
1230 );
1231 }
1232
1233 #[test]
1234 fn disable_same_validator_twice_is_idempotent() {
1235 let mut registry = ValidatorRegistry::with_defaults();
1236 registry.disable_validator("XmlValidator");
1237 registry.disable_validator("XmlValidator");
1238 assert_eq!(registry.disabled_validator_count(), 1);
1239 }
1240
1241 #[test]
1242 fn disable_validator_owned_filters_from_results() {
1243 let mut registry = ValidatorRegistry::with_defaults();
1244 let name = String::from("XmlValidator");
1245 registry.disable_validator_owned(&name);
1246 assert_eq!(registry.disabled_validator_count(), 1);
1247
1248 let skill_validators = registry.validators_for(FileType::Skill);
1249 let names: Vec<&str> = skill_validators.iter().map(|v| v.name()).collect();
1250 assert!(!names.contains(&"XmlValidator"));
1251 }
1252
1253 #[test]
1254 fn disable_validator_owned_twice_is_idempotent() {
1255 let mut registry = ValidatorRegistry::with_defaults();
1256 registry.disable_validator_owned("XmlValidator");
1257 registry.disable_validator_owned("XmlValidator");
1258 assert_eq!(registry.disabled_validator_count(), 1);
1259 }
1260
1261 #[test]
1262 fn mixed_static_and_owned_disable() {
1263 let mut registry = ValidatorRegistry::with_defaults();
1264 registry.disable_validator("XmlValidator");
1265 registry.disable_validator_owned("PromptValidator");
1266 assert_eq!(registry.disabled_validator_count(), 2);
1267
1268 let claude_validators = registry.validators_for(FileType::ClaudeMd);
1269 let names: Vec<&str> = claude_validators.iter().map(|v| v.name()).collect();
1270 assert!(!names.contains(&"XmlValidator"));
1271 assert!(!names.contains(&"PromptValidator"));
1272 }
1273
1274 #[test]
1275 fn builder_without_validator_owned_disables() {
1276 let registry = ValidatorRegistry::builder()
1277 .with_defaults()
1278 .without_validator_owned("XmlValidator")
1279 .build();
1280
1281 let skill_validators = registry.validators_for(FileType::Skill);
1282 let names: Vec<&str> = skill_validators.iter().map(|v| v.name()).collect();
1283 assert!(!names.contains(&"XmlValidator"));
1284 }
1285
1286 struct ProviderA;
1289 impl ValidatorProvider for ProviderA {
1290 fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
1291 vec![(FileType::Skill, skill_validator)]
1292 }
1293 }
1294
1295 struct ProviderB;
1296 impl ValidatorProvider for ProviderB {
1297 fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
1298 vec![(FileType::Agent, agent_validator)]
1299 }
1300 }
1301
1302 #[test]
1303 fn builder_multiple_providers() {
1304 let registry = ValidatorRegistry::builder()
1305 .with_provider(&ProviderA)
1306 .with_provider(&ProviderB)
1307 .build();
1308
1309 assert!(!registry.validators_for(FileType::Skill).is_empty());
1310 assert!(!registry.validators_for(FileType::Agent).is_empty());
1311 assert_eq!(registry.total_validator_count(), 2);
1312 }
1313
1314 #[test]
1317 fn with_defaults_returns_expected_factories() {
1318 let registry = ValidatorRegistry::with_defaults();
1319 assert_eq!(
1320 registry.total_validator_count(),
1321 EXPECTED_BUILTIN_COUNT,
1322 "with_defaults() should register exactly as many validators as EXPECTED_BUILTIN_COUNT"
1323 );
1324 }
1325
1326 #[test]
1327 fn default_trait_matches_with_defaults() {
1328 let via_default = ValidatorRegistry::default();
1329 let via_explicit = ValidatorRegistry::with_defaults();
1330 assert_eq!(
1331 via_default.total_validator_count(),
1332 via_explicit.total_validator_count()
1333 );
1334 }
1335
1336 #[test]
1339 fn every_validatable_file_type_has_at_least_one_validator() {
1340 let validatable_types: [FileType; 41] = [
1341 FileType::Skill,
1342 FileType::ClaudeMd,
1343 FileType::Agent,
1344 FileType::AmpCheck,
1345 FileType::Hooks,
1346 FileType::Plugin,
1347 FileType::Mcp,
1348 FileType::Copilot,
1349 FileType::CopilotScoped,
1350 FileType::CopilotAgent,
1351 FileType::CopilotPrompt,
1352 FileType::CopilotHooks,
1353 FileType::ClaudeRule,
1354 FileType::CursorRule,
1355 FileType::CursorHooks,
1356 FileType::CursorAgent,
1357 FileType::CursorEnvironment,
1358 FileType::CursorRulesLegacy,
1359 FileType::ClineRules,
1360 FileType::ClineRulesFolder,
1361 FileType::OpenCodeConfig,
1362 FileType::GeminiMd,
1363 FileType::GeminiSettings,
1364 FileType::AmpSettings,
1365 FileType::GeminiExtension,
1366 FileType::GeminiIgnore,
1367 FileType::CodexConfig,
1368 FileType::RooRules,
1369 FileType::RooModes,
1370 FileType::RooIgnore,
1371 FileType::RooModeRules,
1372 FileType::RooMcp,
1373 FileType::WindsurfRule,
1374 FileType::WindsurfWorkflow,
1375 FileType::WindsurfRulesLegacy,
1376 FileType::KiroSteering,
1377 FileType::KiroPower,
1378 FileType::KiroAgent,
1379 FileType::KiroHook,
1380 FileType::KiroMcp,
1381 FileType::GenericMarkdown,
1382 ];
1383
1384 for ft in &validatable_types {
1387 match *ft {
1388 FileType::Skill
1389 | FileType::ClaudeMd
1390 | FileType::Agent
1391 | FileType::AmpCheck
1392 | FileType::Hooks
1393 | FileType::Plugin
1394 | FileType::Mcp
1395 | FileType::Copilot
1396 | FileType::CopilotScoped
1397 | FileType::CopilotAgent
1398 | FileType::CopilotPrompt
1399 | FileType::CopilotHooks
1400 | FileType::ClaudeRule
1401 | FileType::CursorRule
1402 | FileType::CursorHooks
1403 | FileType::CursorAgent
1404 | FileType::CursorEnvironment
1405 | FileType::CursorRulesLegacy
1406 | FileType::ClineRules
1407 | FileType::ClineRulesFolder
1408 | FileType::OpenCodeConfig
1409 | FileType::GeminiMd
1410 | FileType::GeminiSettings
1411 | FileType::AmpSettings
1412 | FileType::GeminiExtension
1413 | FileType::GeminiIgnore
1414 | FileType::CodexConfig
1415 | FileType::RooRules
1416 | FileType::RooModes
1417 | FileType::RooIgnore
1418 | FileType::RooModeRules
1419 | FileType::RooMcp
1420 | FileType::WindsurfRule
1421 | FileType::WindsurfWorkflow
1422 | FileType::WindsurfRulesLegacy
1423 | FileType::KiroSteering
1424 | FileType::KiroPower
1425 | FileType::KiroAgent
1426 | FileType::KiroHook
1427 | FileType::KiroMcp
1428 | FileType::GenericMarkdown => (),
1429 FileType::Unknown => {
1430 panic!("Unknown must not appear in validatable_types")
1431 }
1432 }
1433 }
1434
1435 let registry = ValidatorRegistry::with_defaults();
1436
1437 for ft in &validatable_types {
1438 let validators = registry.validators_for(*ft);
1439 assert!(
1440 !validators.is_empty(),
1441 "{ft:?} has no validators registered in the default registry"
1442 );
1443 }
1444 }
1445
1446 #[test]
1447 fn kiro_file_types_route_to_expected_validators() {
1448 let registry = ValidatorRegistry::with_defaults();
1449
1450 let names_for = |file_type: FileType| -> Vec<&'static str> {
1451 registry
1452 .validators_for(file_type)
1453 .iter()
1454 .map(|validator| validator.name())
1455 .collect()
1456 };
1457
1458 assert_eq!(
1459 names_for(FileType::KiroPower),
1460 vec![
1461 "KiroPowerValidator",
1462 "ImportsValidator",
1463 "CrossPlatformValidator",
1464 "XmlValidator"
1465 ]
1466 );
1467 assert_eq!(
1468 names_for(FileType::KiroAgent),
1469 vec!["KiroAgentValidator", "ImportsValidator"]
1470 );
1471 assert_eq!(
1472 names_for(FileType::KiroHook),
1473 vec!["KiroHookValidator", "ImportsValidator"]
1474 );
1475 assert_eq!(names_for(FileType::KiroMcp), vec!["KiroMcpValidator"]);
1476 }
1477
1478 #[test]
1481 fn validators_for_returns_same_slice_on_repeated_calls() {
1482 let registry = ValidatorRegistry::with_defaults();
1483 let first = registry.validators_for(FileType::Skill);
1484 let second = registry.validators_for(FileType::Skill);
1485
1486 assert_eq!(first.len(), second.len());
1488 assert!(
1489 std::ptr::eq(first.as_ptr(), second.as_ptr()),
1490 "validators_for() must return the same cached slice on repeated calls"
1491 );
1492 }
1493
1494 #[test]
1495 fn register_calls_factory_exactly_once() {
1496 ONCE_COUNTING_CONSTRUCTED.store(0, Ordering::SeqCst);
1497
1498 let mut registry = ValidatorRegistry::new();
1499 registry.register(FileType::Skill, once_counting_validator_factory);
1500
1501 assert_eq!(ONCE_COUNTING_CONSTRUCTED.load(Ordering::SeqCst), 1);
1503
1504 let _validators = registry.validators_for(FileType::Skill);
1506 assert_eq!(
1507 ONCE_COUNTING_CONSTRUCTED.load(Ordering::SeqCst),
1508 1,
1509 "validators_for() must not re-instantiate cached validators"
1510 );
1511
1512 let _validators = registry.validators_for(FileType::Skill);
1514 assert_eq!(ONCE_COUNTING_CONSTRUCTED.load(Ordering::SeqCst), 1);
1515 }
1516
1517 #[test]
1518 fn register_calls_factory_exactly_once_via_builder() {
1519 BUILDER_COUNTING_CONSTRUCTED.store(0, Ordering::SeqCst);
1520
1521 let registry = ValidatorRegistry::builder()
1522 .register(FileType::Skill, builder_counting_validator_factory)
1523 .build();
1524
1525 assert_eq!(BUILDER_COUNTING_CONSTRUCTED.load(Ordering::SeqCst), 1);
1527
1528 let _validators = registry.validators_for(FileType::Skill);
1530 assert_eq!(
1531 BUILDER_COUNTING_CONSTRUCTED.load(Ordering::SeqCst),
1532 1,
1533 "validators_for() must not re-instantiate cached validators via builder path"
1534 );
1535 }
1536
1537 #[test]
1538 fn disable_after_construction_removes_from_cache() {
1539 let mut registry = ValidatorRegistry::with_defaults();
1540 let total_before = registry.total_validator_count();
1541
1542 let before = registry.validators_for(FileType::Skill);
1544 assert!(
1545 before.iter().any(|v| v.name() == "XmlValidator"),
1546 "XmlValidator should be present before disabling"
1547 );
1548
1549 registry.disable_validator("XmlValidator");
1550
1551 let after = registry.validators_for(FileType::Skill);
1553 assert!(
1554 !after.iter().any(|v| v.name() == "XmlValidator"),
1555 "XmlValidator should be removed after disable_validator()"
1556 );
1557
1558 let claude_after = registry.validators_for(FileType::ClaudeMd);
1560 assert!(
1561 !claude_after.iter().any(|v| v.name() == "XmlValidator"),
1562 "XmlValidator should be removed from all file types"
1563 );
1564
1565 let xml_occurrences = BuiltinProvider
1569 .named_validators()
1570 .iter()
1571 .filter(|(_, name, _)| *name == Some("XmlValidator"))
1572 .count();
1573 assert_eq!(
1574 xml_occurrences, 10,
1575 "Expected XmlValidator in 10 BuiltinProvider entries"
1576 );
1577 let total_after = registry.total_validator_count();
1578 assert_eq!(
1579 total_before - total_after,
1580 xml_occurrences,
1581 "Disabling XmlValidator should remove exactly {} instances, \
1582 but removed {}",
1583 xml_occurrences,
1584 total_before - total_after
1585 );
1586 }
1587
1588 #[test]
1589 fn registry_is_send_sync() {
1590 fn assert_send_sync<T: Send + Sync>() {}
1591 assert_send_sync::<ValidatorRegistry>();
1592 }
1593
1594 #[test]
1595 #[allow(deprecated)]
1596 fn deprecated_total_factory_count_matches_total_validator_count() {
1597 let registry = ValidatorRegistry::with_defaults();
1598 assert_eq!(
1599 registry.total_factory_count(),
1600 registry.total_validator_count()
1601 );
1602 }
1603
1604 #[test]
1607 fn defaults_names_match_factory_names() {
1608 for (file_type, static_name, factory) in BuiltinProvider.named_validators() {
1612 let static_name = static_name.expect("BuiltinProvider entries must have Some(name)");
1613 let instance = factory();
1614 let runtime_name = instance.name();
1615 assert_eq!(
1616 static_name, runtime_name,
1617 "BuiltinProvider name mismatch for {file_type:?}: \
1618 static=\"{static_name}\" vs runtime=\"{runtime_name}\""
1619 );
1620 }
1621 }
1622
1623 static NAMED_SKIP_COUNTING_CONSTRUCTED: AtomicUsize = AtomicUsize::new(0);
1625
1626 struct NamedSkipCountingValidator;
1627
1628 impl Validator for NamedSkipCountingValidator {
1629 fn validate(
1630 &self,
1631 _path: &std::path::Path,
1632 _content: &str,
1633 _config: &crate::config::LintConfig,
1634 ) -> Vec<crate::diagnostics::Diagnostic> {
1635 Vec::new()
1636 }
1637
1638 fn name(&self) -> &'static str {
1639 "NamedSkipCountingValidator"
1640 }
1641 }
1642
1643 fn named_skip_counting_validator_factory() -> Box<dyn Validator> {
1644 NAMED_SKIP_COUNTING_CONSTRUCTED.fetch_add(1, Ordering::SeqCst);
1645 Box::new(NamedSkipCountingValidator)
1646 }
1647
1648 #[test]
1649 fn named_disabled_validator_skips_factory_call() {
1650 NAMED_SKIP_COUNTING_CONSTRUCTED.store(0, Ordering::SeqCst);
1653
1654 struct NamedCountingProvider;
1655 impl ValidatorProvider for NamedCountingProvider {
1656 fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
1657 vec![(FileType::Skill, named_skip_counting_validator_factory)]
1658 }
1659
1660 fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
1661 vec![(
1662 FileType::Skill,
1663 Some("NamedSkipCountingValidator"),
1664 named_skip_counting_validator_factory,
1665 )]
1666 }
1667 }
1668
1669 let registry = ValidatorRegistry::builder()
1670 .with_provider(&NamedCountingProvider)
1671 .without_validator("NamedSkipCountingValidator")
1672 .build();
1673
1674 assert_eq!(
1677 NAMED_SKIP_COUNTING_CONSTRUCTED.load(Ordering::SeqCst),
1678 0,
1679 "register_named must not call factory for disabled validators"
1680 );
1681
1682 assert!(registry.validators_for(FileType::Skill).is_empty());
1684 }
1685
1686 #[test]
1687 fn builtin_provider_named_validators_returns_all_names() {
1688 let provider = BuiltinProvider;
1689 let named = provider.named_validators();
1690
1691 assert_eq!(
1692 named.len(),
1693 EXPECTED_BUILTIN_COUNT,
1694 "named_validators() should return EXPECTED_BUILTIN_COUNT entries"
1695 );
1696
1697 for (i, (ft, name, _factory)) in named.iter().enumerate() {
1699 assert!(
1700 name.is_some(),
1701 "Entry {i} ({ft:?}) should have Some(name), got None"
1702 );
1703 }
1704
1705 let unnamed = provider.validators();
1708 assert_eq!(
1709 unnamed.len(),
1710 named.len(),
1711 "validators() and named_validators() must return the same count"
1712 );
1713 for ((ft_unnamed, _), (ft_named, _, _)) in unnamed.iter().zip(named.iter()) {
1714 assert_eq!(
1715 ft_unnamed, ft_named,
1716 "validators() and named_validators() file types must match"
1717 );
1718 }
1719 }
1720
1721 #[test]
1722 fn custom_provider_named_validators_defaults_to_none() {
1723 let provider = TestProvider;
1726 let named = provider.named_validators();
1727
1728 assert_eq!(named.len(), 1);
1729 let (ft, name, _factory) = &named[0];
1730 assert_eq!(*ft, FileType::Skill);
1731 assert!(
1732 name.is_none(),
1733 "Default named_validators() should yield None names"
1734 );
1735 }
1736
1737 struct MismatchedValidator;
1742
1743 impl Validator for MismatchedValidator {
1744 fn validate(
1745 &self,
1746 _path: &std::path::Path,
1747 _content: &str,
1748 _config: &crate::config::LintConfig,
1749 ) -> Vec<crate::diagnostics::Diagnostic> {
1750 vec![]
1751 }
1752
1753 fn name(&self) -> &'static str {
1754 "ActualName"
1755 }
1756 }
1757
1758 fn mismatched_validator_factory() -> Box<dyn Validator> {
1759 Box::new(MismatchedValidator)
1760 }
1761
1762 #[cfg(debug_assertions)]
1763 #[test]
1764 #[should_panic(expected = "name/factory mismatch")]
1765 fn mismatched_named_validator_panics_in_debug() {
1766 struct MismatchedProvider;
1770 impl ValidatorProvider for MismatchedProvider {
1771 fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
1772 vec![(FileType::Skill, mismatched_validator_factory)]
1774 }
1775 fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
1776 vec![(
1777 FileType::Skill,
1778 Some("WrongName"),
1779 mismatched_validator_factory,
1780 )]
1781 }
1782 }
1783
1784 let _registry = ValidatorRegistry::builder()
1787 .with_provider(&MismatchedProvider)
1788 .build();
1789 }
1790
1791 #[test]
1795 fn mismatched_named_validator_silently_skips_when_disabled() {
1796 struct MismatchedProvider;
1803 impl ValidatorProvider for MismatchedProvider {
1804 fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
1805 vec![(FileType::Skill, mismatched_validator_factory)]
1806 }
1807 fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
1808 vec![(
1809 FileType::Skill,
1810 Some("WrongName"),
1811 mismatched_validator_factory,
1812 )]
1813 }
1814 }
1815
1816 let registry = ValidatorRegistry::builder()
1817 .with_provider(&MismatchedProvider)
1818 .without_validator("WrongName")
1819 .build();
1820
1821 assert!(
1824 registry.validators_for(FileType::Skill).is_empty(),
1825 "Mismatched static name caused silent skip - no validators registered"
1826 );
1827 }
1828
1829 #[cfg(not(debug_assertions))]
1833 #[test]
1834 fn mismatched_named_validator_slip_through_when_real_name_disabled() {
1835 struct MismatchedProvider;
1841 impl ValidatorProvider for MismatchedProvider {
1842 fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
1843 vec![(FileType::Skill, mismatched_validator_factory)]
1845 }
1846 fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
1847 vec![(
1848 FileType::Skill,
1849 Some("WrongName"),
1850 mismatched_validator_factory,
1851 )]
1852 }
1853 }
1854
1855 let registry = ValidatorRegistry::builder()
1856 .with_provider(&MismatchedProvider)
1857 .without_validator("ActualName") .build();
1859
1860 let slipped_through = registry.validators_for(FileType::Skill);
1865 assert_eq!(
1866 slipped_through.len(),
1867 1,
1868 "Mismatched static name caused validator to slip through despite disable request"
1869 );
1870 assert_eq!(
1871 slipped_through[0].name(),
1872 "ActualName",
1873 "The registered validator must be the mismatched one"
1874 );
1875 }
1876
1877 #[test]
1880 fn skill_provider_count() {
1881 assert_eq!(SkillProvider.named_validators().len(), 4);
1882 }
1883
1884 #[test]
1885 fn claude_provider_count() {
1886 assert_eq!(ClaudeProvider.named_validators().len(), 11);
1887 }
1888
1889 #[test]
1890 fn copilot_provider_count() {
1891 assert_eq!(CopilotProvider.named_validators().len(), 9);
1892 }
1893
1894 #[test]
1895 fn cursor_provider_count() {
1896 assert_eq!(CursorProvider.named_validators().len(), 9);
1897 }
1898
1899 #[test]
1900 fn gemini_provider_count() {
1901 assert_eq!(GeminiProvider.named_validators().len(), 8);
1902 }
1903
1904 #[test]
1905 fn roo_provider_count() {
1906 assert_eq!(RooProvider.named_validators().len(), 5);
1907 }
1908
1909 #[test]
1910 fn windsurf_provider_count() {
1911 assert_eq!(WindsurfProvider.named_validators().len(), 3);
1912 }
1913
1914 #[test]
1915 fn misc_provider_count() {
1916 assert_eq!(MiscProvider.named_validators().len(), 22);
1917 }
1918
1919 #[test]
1920 fn all_category_providers_sum_to_expected_count() {
1921 let total = SkillProvider.named_validators().len()
1922 + ClaudeProvider.named_validators().len()
1923 + CopilotProvider.named_validators().len()
1924 + CursorProvider.named_validators().len()
1925 + GeminiProvider.named_validators().len()
1926 + RooProvider.named_validators().len()
1927 + WindsurfProvider.named_validators().len()
1928 + MiscProvider.named_validators().len();
1929 assert_eq!(
1930 total, EXPECTED_BUILTIN_COUNT,
1931 "Sum of all category provider counts must equal EXPECTED_BUILTIN_COUNT"
1932 );
1933 }
1934
1935 #[test]
1936 fn all_category_provider_entries_have_names() {
1937 let providers: &[&dyn ValidatorProvider] = &[
1938 &SkillProvider,
1939 &ClaudeProvider,
1940 &CopilotProvider,
1941 &CursorProvider,
1942 &GeminiProvider,
1943 &RooProvider,
1944 &WindsurfProvider,
1945 &MiscProvider,
1946 ];
1947 for provider in providers {
1948 for (i, (ft, name, _)) in provider.named_validators().iter().enumerate() {
1949 assert!(
1950 name.is_some(),
1951 "{}: entry {i} ({ft:?}) should have Some(name), got None",
1952 provider.name()
1953 );
1954 }
1955 }
1956 }
1957
1958 #[test]
1959 fn category_provider_validators_count_matches_named() {
1960 let providers: &[&dyn ValidatorProvider] = &[
1961 &SkillProvider,
1962 &ClaudeProvider,
1963 &CopilotProvider,
1964 &CursorProvider,
1965 &GeminiProvider,
1966 &RooProvider,
1967 &WindsurfProvider,
1968 &MiscProvider,
1969 ];
1970 for provider in providers {
1971 assert_eq!(
1972 provider.validators().len(),
1973 provider.named_validators().len(),
1974 "{}: validators() and named_validators() counts must match",
1975 provider.name()
1976 );
1977 }
1978 }
1979
1980 #[test]
1981 fn claude_provider_includes_codex_on_claude_md() {
1982 let entries = ClaudeProvider.named_validators();
1984 let has_codex_on_claude_md = entries
1985 .iter()
1986 .any(|(ft, name, _)| *ft == FileType::ClaudeMd && *name == Some("CodexValidator"));
1987 assert!(
1988 has_codex_on_claude_md,
1989 "ClaudeProvider must include CodexValidator on ClaudeMd (CDX-003)"
1990 );
1991 }
1992
1993 #[test]
1994 fn codex_validator_only_on_expected_file_types() {
1995 let entries = BuiltinProvider.named_validators();
1997 for (ft, name, _) in &entries {
1998 if *name == Some("CodexValidator") {
1999 assert!(
2000 *ft == FileType::ClaudeMd,
2001 "CodexValidator must only be registered for ClaudeMd, found {:?}",
2002 ft
2003 );
2004 }
2005 }
2006 let codex_count = entries
2007 .iter()
2008 .filter(|(_, name, _)| *name == Some("CodexValidator"))
2009 .count();
2010 assert_eq!(
2011 codex_count, 1,
2012 "CodexValidator should appear exactly once (ClaudeMd)"
2013 );
2014 }
2015
2016 #[test]
2017 fn codex_config_validator_only_on_codex_config() {
2018 let entries = BuiltinProvider.named_validators();
2019 for (ft, name, _) in &entries {
2020 if *name == Some("CodexConfigValidator") {
2021 assert_eq!(
2022 *ft,
2023 FileType::CodexConfig,
2024 "CodexConfigValidator must only be registered for CodexConfig"
2025 );
2026 }
2027 }
2028 let count = entries
2029 .iter()
2030 .filter(|(_, name, _)| *name == Some("CodexConfigValidator"))
2031 .count();
2032 assert_eq!(
2033 count, 1,
2034 "CodexConfigValidator should appear exactly once (CodexConfig)"
2035 );
2036 }
2037
2038 #[test]
2039 fn builder_second_build_has_no_disabled_validators() {
2040 let mut builder = ValidatorRegistry::builder();
2043 builder.with_defaults().without_validator("XmlValidator");
2044
2045 let first = builder.build();
2046 let second = builder.build();
2047
2048 let xml_count = BuiltinProvider
2050 .named_validators()
2051 .iter()
2052 .filter(|(_, name, _)| *name == Some("XmlValidator"))
2053 .count();
2054 assert_eq!(
2055 second.total_validator_count() - first.total_validator_count(),
2056 xml_count,
2057 "First registry should have exactly {xml_count} fewer validators (one per XmlValidator registration)"
2058 );
2059 assert_eq!(
2061 second.total_validator_count(),
2062 EXPECTED_BUILTIN_COUNT,
2063 "Second build() must produce a full registry (disabled set was consumed by first build)"
2064 );
2065 }
2066
2067 #[test]
2068 fn builtin_provider_output_matches_sub_provider_concatenation() {
2069 let expected: Vec<_> = [
2072 SkillProvider.named_validators(),
2073 ClaudeProvider.named_validators(),
2074 CopilotProvider.named_validators(),
2075 CursorProvider.named_validators(),
2076 GeminiProvider.named_validators(),
2077 RooProvider.named_validators(),
2078 WindsurfProvider.named_validators(),
2079 MiscProvider.named_validators(),
2080 ]
2081 .into_iter()
2082 .flatten()
2083 .collect();
2084
2085 let actual = BuiltinProvider.named_validators();
2086
2087 assert_eq!(
2088 actual.len(),
2089 expected.len(),
2090 "BuiltinProvider entry count must equal sum of sub-providers"
2091 );
2092 for (i, ((aft, aname, _), (eft, ename, _))) in
2093 actual.iter().zip(expected.iter()).enumerate()
2094 {
2095 assert_eq!(
2096 aft, eft,
2097 "Entry {i}: file type mismatch (actual={aft:?}, expected={eft:?})"
2098 );
2099 assert_eq!(
2100 aname, ename,
2101 "Entry {i}: name mismatch (actual={aname:?}, expected={ename:?})"
2102 );
2103 }
2104 }
2105
2106 #[test]
2107 fn builder_with_defaults_called_twice_registers_all_validators_twice() {
2108 let registry = ValidatorRegistry::builder()
2112 .with_defaults()
2113 .with_defaults()
2114 .build();
2115 assert_eq!(
2116 registry.total_validator_count(),
2117 EXPECTED_BUILTIN_COUNT * 2,
2118 "Calling with_defaults() twice must register all validators twice"
2119 );
2120 }
2121}