1use std::path::PathBuf;
30use thiserror::Error;
31
32#[derive(Debug, Error)]
42pub enum SkillsError {
43 #[error("I/O error: {0}")]
57 IoError(#[from] std::io::Error),
58
59 #[error("YAML parsing error: {0}")]
72 YamlError(String),
73
74 #[error("Validation error: {0}")]
87 ValidationError(String),
88
89 #[error("Path traversal security violation: {0}")]
104 PathTraversalError(String),
105}
106
107impl From<serde_yaml::Error> for SkillsError {
109 fn from(err: serde_yaml::Error) -> Self {
110 SkillsError::YamlError(err.to_string())
111 }
112}
113
114#[derive(Debug, Clone)]
123pub struct SkillsCopyResult {
124 pub files_copied: usize,
126
127 pub files_skipped: usize,
129
130 pub warnings: Vec<String>,
132
133 pub destination_path: PathBuf,
135}
136
137#[derive(Debug, Clone)]
142pub struct SkillsValidation {
143 pub is_valid: bool,
145
146 pub missing_required: Vec<String>,
148
149 pub missing_optional: Vec<String>,
151
152 pub total_rule_files: usize,
154}
155
156#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
161pub struct SkillsIndex {
162 pub skills_by_tag: std::collections::HashMap<String, Vec<String>>,
167
168 pub total_skills: usize,
170}
171
172#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
177pub struct SkillFile {
178 pub path: String,
182
183 pub title: String,
187
188 pub tags: Vec<String>,
193
194 pub has_frontmatter: bool,
196}
197
198#[cfg(test)]
203mod tests {
204 use super::*;
205 use std::io;
206
207 #[test]
208 fn test_io_error_conversion() {
209 let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
210 let skills_error: SkillsError = io_error.into();
211
212 match skills_error {
213 SkillsError::IoError(_) => (),
214 _ => panic!("Expected IoError variant"),
215 }
216 }
217
218 #[test]
219 fn test_yaml_error_display() {
220 let error = SkillsError::YamlError("Invalid YAML syntax".to_string());
221 let display = format!("{}", error);
222
223 assert!(display.contains("YAML parsing error"));
224 assert!(display.contains("Invalid YAML syntax"));
225 }
226
227 #[test]
228 fn test_validation_error_display() {
229 let error = SkillsError::ValidationError("Missing SKILL.md".to_string());
230 let display = format!("{}", error);
231
232 assert!(display.contains("Validation error"));
233 assert!(display.contains("Missing SKILL.md"));
234 }
235
236 #[test]
237 fn test_path_traversal_error_display() {
238 let error = SkillsError::PathTraversalError(
239 "Path contains '..' components".to_string()
240 );
241 let display = format!("{}", error);
242
243 assert!(display.contains("Path traversal"));
244 assert!(display.contains("'..'"));
245 }
246
247 #[test]
248 fn test_serde_yaml_error_conversion() {
249 let invalid_yaml = "invalid: yaml: syntax:";
251 let yaml_error = serde_yaml::from_str::<serde_yaml::Value>(invalid_yaml)
252 .unwrap_err();
253
254 let skills_error: SkillsError = yaml_error.into();
255
256 match skills_error {
257 SkillsError::YamlError(_) => (),
258 _ => panic!("Expected YamlError variant"),
259 }
260 }
261
262 #[test]
263 fn test_skills_copy_result_creation() {
264 let result = SkillsCopyResult {
265 files_copied: 31,
266 files_skipped: 0,
267 warnings: vec!["Optional file missing".to_string()],
268 destination_path: PathBuf::from("/workspace/.kiro/steering/composio"),
269 };
270
271 assert_eq!(result.files_copied, 31);
272 assert_eq!(result.files_skipped, 0);
273 assert_eq!(result.warnings.len(), 1);
274 }
275
276 #[test]
277 fn test_skills_validation_creation() {
278 let validation = SkillsValidation {
279 is_valid: true,
280 missing_required: vec![],
281 missing_optional: vec!["optional.md".to_string()],
282 total_rule_files: 29,
283 };
284
285 assert!(validation.is_valid);
286 assert_eq!(validation.missing_required.len(), 0);
287 assert_eq!(validation.missing_optional.len(), 1);
288 assert_eq!(validation.total_rule_files, 29);
289 }
290
291 #[test]
292 fn test_error_is_send_and_sync() {
293 fn assert_send_sync<T: Send + Sync>() {}
294 assert_send_sync::<SkillsError>();
295 }
296
297 #[test]
298 fn test_skills_copy_result_is_clone() {
299 let result = SkillsCopyResult {
300 files_copied: 10,
301 files_skipped: 5,
302 warnings: vec![],
303 destination_path: PathBuf::from("/test"),
304 };
305
306 let cloned = result.clone();
307 assert_eq!(cloned.files_copied, result.files_copied);
308 assert_eq!(cloned.files_skipped, result.files_skipped);
309 }
310
311 #[test]
312 fn test_skills_validation_is_clone() {
313 let validation = SkillsValidation {
314 is_valid: false,
315 missing_required: vec!["SKILL.md".to_string()],
316 missing_optional: vec![],
317 total_rule_files: 0,
318 };
319
320 let cloned = validation.clone();
321 assert_eq!(cloned.is_valid, validation.is_valid);
322 assert_eq!(cloned.missing_required, validation.missing_required);
323 }
324
325 #[test]
326 fn test_skills_index_creation() {
327 use std::collections::HashMap;
328
329 let mut skills_by_tag = HashMap::new();
330 skills_by_tag.insert(
331 "tool-router".to_string(),
332 vec!["rules/tr-userid-best-practices.md".to_string()],
333 );
334 skills_by_tag.insert(
335 "security".to_string(),
336 vec![
337 "rules/tr-userid-best-practices.md".to_string(),
338 "rules/tr-auth-auto.md".to_string(),
339 ],
340 );
341
342 let index = SkillsIndex {
343 skills_by_tag,
344 total_skills: 29,
345 };
346
347 assert_eq!(index.total_skills, 29);
348 assert_eq!(index.skills_by_tag.len(), 2);
349 assert_eq!(index.skills_by_tag.get("security").unwrap().len(), 2);
350 }
351
352 #[test]
353 fn test_skills_index_serialization() {
354 use std::collections::HashMap;
355
356 let mut skills_by_tag = HashMap::new();
357 skills_by_tag.insert(
358 "test-tag".to_string(),
359 vec!["test-file.md".to_string()],
360 );
361
362 let index = SkillsIndex {
363 skills_by_tag,
364 total_skills: 1,
365 };
366
367 let json = serde_json::to_string(&index).unwrap();
369 assert!(json.contains("test-tag"));
370 assert!(json.contains("test-file.md"));
371
372 let deserialized: SkillsIndex = serde_json::from_str(&json).unwrap();
374 assert_eq!(deserialized.total_skills, 1);
375 assert_eq!(deserialized.skills_by_tag.len(), 1);
376 }
377
378 #[test]
379 fn test_skill_file_creation() {
380 let skill_file = SkillFile {
381 path: "rules/tr-userid-best-practices.md".to_string(),
382 title: "Choose User IDs Carefully for Security and Isolation".to_string(),
383 tags: vec![
384 "tool-router".to_string(),
385 "user-id".to_string(),
386 "security".to_string(),
387 ],
388 has_frontmatter: true,
389 };
390
391 assert_eq!(skill_file.path, "rules/tr-userid-best-practices.md");
392 assert_eq!(skill_file.tags.len(), 3);
393 assert!(skill_file.has_frontmatter);
394 }
395
396 #[test]
397 fn test_skill_file_serialization() {
398 let skill_file = SkillFile {
399 path: "test.md".to_string(),
400 title: "Test Skill".to_string(),
401 tags: vec!["test".to_string()],
402 has_frontmatter: true,
403 };
404
405 let json = serde_json::to_string(&skill_file).unwrap();
407 assert!(json.contains("test.md"));
408 assert!(json.contains("Test Skill"));
409
410 let deserialized: SkillFile = serde_json::from_str(&json).unwrap();
412 assert_eq!(deserialized.path, "test.md");
413 assert_eq!(deserialized.title, "Test Skill");
414 assert_eq!(deserialized.tags.len(), 1);
415 }
416
417 #[test]
418 fn test_skills_index_is_clone() {
419 use std::collections::HashMap;
420
421 let mut skills_by_tag = HashMap::new();
422 skills_by_tag.insert("tag1".to_string(), vec!["file1.md".to_string()]);
423
424 let index = SkillsIndex {
425 skills_by_tag,
426 total_skills: 1,
427 };
428
429 let cloned = index.clone();
430 assert_eq!(cloned.total_skills, index.total_skills);
431 assert_eq!(cloned.skills_by_tag.len(), index.skills_by_tag.len());
432 }
433
434 #[test]
435 fn test_skill_file_is_clone() {
436 let skill_file = SkillFile {
437 path: "test.md".to_string(),
438 title: "Test".to_string(),
439 tags: vec!["tag1".to_string()],
440 has_frontmatter: false,
441 };
442
443 let cloned = skill_file.clone();
444 assert_eq!(cloned.path, skill_file.path);
445 assert_eq!(cloned.title, skill_file.title);
446 assert_eq!(cloned.has_frontmatter, skill_file.has_frontmatter);
447 }
448}
449
450pub async fn validate_skills_structure(
506 vendor_dir: &std::path::Path,
507) -> Result<SkillsValidation, SkillsError> {
508 use tokio::fs;
509
510 if !vendor_dir.exists() {
512 return Ok(SkillsValidation {
513 is_valid: false,
514 missing_required: vec![
515 format!("Source directory not found: {}", vendor_dir.display())
516 ],
517 missing_optional: vec![],
518 total_rule_files: 0,
519 });
520 }
521
522 let metadata = fs::metadata(vendor_dir).await?;
524 if !metadata.is_dir() {
525 return Ok(SkillsValidation {
526 is_valid: false,
527 missing_required: vec![
528 format!("Path is not a directory: {}", vendor_dir.display())
529 ],
530 missing_optional: vec![],
531 total_rule_files: 0,
532 });
533 }
534
535 let mut missing_required = Vec::new();
536 let mut missing_optional = Vec::new();
537
538 let skill_md = vendor_dir.join("SKILL.md");
540 if !skill_md.exists() {
541 missing_required.push("SKILL.md".to_string());
542 }
543
544 let agents_md = vendor_dir.join("AGENTS.md");
546 if !agents_md.exists() {
547 missing_required.push("AGENTS.md".to_string());
548 }
549
550 let rules_dir = vendor_dir.join("rules");
552 let mut total_rule_files = 0;
553
554 if !rules_dir.exists() {
555 missing_required.push("rules/ directory".to_string());
556 } else {
557 let rules_metadata = fs::metadata(&rules_dir).await?;
559 if !rules_metadata.is_dir() {
560 missing_required.push("rules/ is not a directory".to_string());
561 } else {
562 let mut entries = fs::read_dir(&rules_dir).await?;
564
565 while let Some(entry) = entries.next_entry().await? {
566 let path = entry.path();
567
568 if path.is_file() {
570 if let Some(extension) = path.extension() {
571 if extension == "md" {
572 total_rule_files += 1;
573 }
574 }
575 }
576 }
577
578 if total_rule_files == 0 {
580 missing_optional.push(
581 "No markdown files found in rules/ directory".to_string()
582 );
583 }
584 }
585 }
586
587 let is_valid = missing_required.is_empty();
589
590 Ok(SkillsValidation {
591 is_valid,
592 missing_required,
593 missing_optional,
594 total_rule_files,
595 })
596}
597
598#[cfg(test)]
603mod validation_tests {
604 use super::*;
605 use std::path::PathBuf;
606 use tokio::fs;
607
608 async fn create_test_dir() -> Result<tempfile::TempDir, Box<dyn std::error::Error>> {
610 let temp_dir = tempfile::tempdir()?;
611 Ok(temp_dir)
612 }
613
614 async fn create_test_skills_structure(
616 base_dir: &std::path::Path,
617 include_skill_md: bool,
618 include_agents_md: bool,
619 include_rules_dir: bool,
620 num_rule_files: usize,
621 ) -> Result<(), Box<dyn std::error::Error>> {
622 if include_skill_md {
624 let skill_path = base_dir.join("SKILL.md");
625 fs::write(&skill_path, "# Composio Skills\n\nTest content").await?;
626 }
627
628 if include_agents_md {
630 let agents_path = base_dir.join("AGENTS.md");
631 fs::write(&agents_path, "# Agents\n\nTest content").await?;
632 }
633
634 if include_rules_dir {
636 let rules_dir = base_dir.join("rules");
637 fs::create_dir(&rules_dir).await?;
638
639 for i in 0..num_rule_files {
641 let rule_path = rules_dir.join(format!("rule-{}.md", i));
642 fs::write(&rule_path, format!("# Rule {}\n\nTest content", i)).await?;
643 }
644 }
645
646 Ok(())
647 }
648
649 #[tokio::test]
650 async fn test_validate_skills_structure_with_valid_directory() {
651 let temp_dir = create_test_dir().await.unwrap();
652 let vendor_path = temp_dir.path();
653
654 create_test_skills_structure(vendor_path, true, true, true, 5)
656 .await
657 .unwrap();
658
659 let validation = validate_skills_structure(vendor_path).await.unwrap();
660
661 assert!(validation.is_valid);
662 assert_eq!(validation.missing_required.len(), 0);
663 assert_eq!(validation.total_rule_files, 5);
664 }
665
666 #[tokio::test]
667 async fn test_validate_skills_structure_missing_skill_md() {
668 let temp_dir = create_test_dir().await.unwrap();
669 let vendor_path = temp_dir.path();
670
671 create_test_skills_structure(vendor_path, false, true, true, 3)
673 .await
674 .unwrap();
675
676 let validation = validate_skills_structure(vendor_path).await.unwrap();
677
678 assert!(!validation.is_valid);
679 assert!(validation.missing_required.contains(&"SKILL.md".to_string()));
680 assert_eq!(validation.total_rule_files, 3);
681 }
682
683 #[tokio::test]
684 async fn test_validate_skills_structure_missing_agents_md() {
685 let temp_dir = create_test_dir().await.unwrap();
686 let vendor_path = temp_dir.path();
687
688 create_test_skills_structure(vendor_path, true, false, true, 3)
690 .await
691 .unwrap();
692
693 let validation = validate_skills_structure(vendor_path).await.unwrap();
694
695 assert!(!validation.is_valid);
696 assert!(validation.missing_required.contains(&"AGENTS.md".to_string()));
697 }
698
699 #[tokio::test]
700 async fn test_validate_skills_structure_missing_rules_directory() {
701 let temp_dir = create_test_dir().await.unwrap();
702 let vendor_path = temp_dir.path();
703
704 create_test_skills_structure(vendor_path, true, true, false, 0)
706 .await
707 .unwrap();
708
709 let validation = validate_skills_structure(vendor_path).await.unwrap();
710
711 assert!(!validation.is_valid);
712 assert!(validation
713 .missing_required
714 .iter()
715 .any(|s| s.contains("rules/")));
716 assert_eq!(validation.total_rule_files, 0);
717 }
718
719 #[tokio::test]
720 async fn test_validate_skills_structure_empty_rules_directory() {
721 let temp_dir = create_test_dir().await.unwrap();
722 let vendor_path = temp_dir.path();
723
724 create_test_skills_structure(vendor_path, true, true, true, 0)
726 .await
727 .unwrap();
728
729 let validation = validate_skills_structure(vendor_path).await.unwrap();
730
731 assert!(validation.is_valid);
733 assert!(validation
734 .missing_optional
735 .iter()
736 .any(|s| s.contains("No markdown files")));
737 assert_eq!(validation.total_rule_files, 0);
738 }
739
740 #[tokio::test]
741 async fn test_validate_skills_structure_nonexistent_directory() {
742 let nonexistent_path = PathBuf::from("/nonexistent/path/to/skills");
743
744 let validation = validate_skills_structure(&nonexistent_path)
745 .await
746 .unwrap();
747
748 assert!(!validation.is_valid);
749 assert!(validation
750 .missing_required
751 .iter()
752 .any(|s| s.contains("Source directory not found")));
753 }
754
755 #[tokio::test]
756 async fn test_validate_skills_structure_path_is_file() {
757 let temp_dir = create_test_dir().await.unwrap();
758 let file_path = temp_dir.path().join("not-a-directory.txt");
759
760 fs::write(&file_path, "test content").await.unwrap();
762
763 let validation = validate_skills_structure(&file_path).await.unwrap();
764
765 assert!(!validation.is_valid);
766 assert!(validation
767 .missing_required
768 .iter()
769 .any(|s| s.contains("not a directory")));
770 }
771
772 #[tokio::test]
773 async fn test_validate_skills_structure_multiple_missing_files() {
774 let temp_dir = create_test_dir().await.unwrap();
775 let vendor_path = temp_dir.path();
776
777 create_test_skills_structure(vendor_path, false, false, false, 0)
779 .await
780 .unwrap();
781
782 let validation = validate_skills_structure(vendor_path).await.unwrap();
783
784 assert!(!validation.is_valid);
785 assert!(validation.missing_required.len() >= 3);
786 assert!(validation.missing_required.contains(&"SKILL.md".to_string()));
787 assert!(validation.missing_required.contains(&"AGENTS.md".to_string()));
788 assert!(validation
789 .missing_required
790 .iter()
791 .any(|s| s.contains("rules/")));
792 }
793
794 #[tokio::test]
795 async fn test_validate_skills_structure_with_many_rule_files() {
796 let temp_dir = create_test_dir().await.unwrap();
797 let vendor_path = temp_dir.path();
798
799 create_test_skills_structure(vendor_path, true, true, true, 29)
801 .await
802 .unwrap();
803
804 let validation = validate_skills_structure(vendor_path).await.unwrap();
805
806 assert!(validation.is_valid);
807 assert_eq!(validation.missing_required.len(), 0);
808 assert_eq!(validation.total_rule_files, 29);
809 }
810
811 #[tokio::test]
812 async fn test_validate_skills_structure_rules_is_file_not_directory() {
813 let temp_dir = create_test_dir().await.unwrap();
814 let vendor_path = temp_dir.path();
815
816 create_test_skills_structure(vendor_path, true, true, false, 0)
818 .await
819 .unwrap();
820
821 let rules_path = vendor_path.join("rules");
823 fs::write(&rules_path, "not a directory").await.unwrap();
824
825 let validation = validate_skills_structure(vendor_path).await.unwrap();
826
827 assert!(!validation.is_valid);
828 assert!(validation
829 .missing_required
830 .iter()
831 .any(|s| s.contains("rules/") && s.contains("not a directory")));
832 }
833}
834
835pub fn add_auto_inclusion_frontmatter(content: &str) -> Result<String, SkillsError> {
883 use serde_yaml::Value;
884
885 if content.starts_with("---") {
887 let content_after_first_delimiter = &content[3..];
889
890 if let Some(end_pos) = content_after_first_delimiter.find("\n---") {
891 let frontmatter_yaml = &content_after_first_delimiter[..end_pos];
893
894 let body_start = 3 + end_pos + 4; let body = if body_start < content.len() {
897 &content[body_start..]
898 } else {
899 ""
900 };
901
902 let mut frontmatter: serde_yaml::Mapping = serde_yaml::from_str(frontmatter_yaml)
904 .map_err(|e| SkillsError::YamlError(format!("Failed to parse frontmatter: {}", e)))?;
905
906 frontmatter.insert(
908 Value::String("inclusion".to_string()),
909 Value::String("auto".to_string()),
910 );
911
912 let new_yaml = serde_yaml::to_string(&frontmatter)
914 .map_err(|e| SkillsError::YamlError(format!("Failed to serialize frontmatter: {}", e)))?;
915
916 Ok(format!("---\n{}---{}", new_yaml, body))
918 } else {
919 Err(SkillsError::YamlError(
921 "Malformed frontmatter: missing closing '---' delimiter".to_string()
922 ))
923 }
924 } else {
925 let frontmatter = "---\ninclusion: auto\n---\n\n";
927 Ok(format!("{}{}", frontmatter, content))
928 }
929}
930
931#[cfg(test)]
936mod frontmatter_tests {
937 use super::*;
938
939 #[test]
940 fn test_add_frontmatter_to_file_without_frontmatter() {
941 let content = "# My Skill\n\nThis is a skill file.";
942 let result = add_auto_inclusion_frontmatter(content).unwrap();
943
944 assert!(result.starts_with("---\n"));
945 assert!(result.contains("inclusion: auto"));
946 assert!(result.contains("# My Skill"));
947 assert!(result.contains("This is a skill file."));
948 }
949
950 #[test]
951 fn test_add_frontmatter_to_empty_file() {
952 let content = "";
953 let result = add_auto_inclusion_frontmatter(content).unwrap();
954
955 assert!(result.starts_with("---\n"));
956 assert!(result.contains("inclusion: auto"));
957 assert!(result.ends_with("---\n\n"));
958 }
959
960 #[test]
961 fn test_preserve_existing_frontmatter_fields() {
962 let content = r#"---
963title: My Skill
964impact: HIGH
965description: A test skill
966tags:
967 - test
968 - example
969---
970
971# Content here"#;
972
973 let result = add_auto_inclusion_frontmatter(content).unwrap();
974
975 assert!(result.contains("inclusion: auto"));
976 assert!(result.contains("title: My Skill"));
977 assert!(result.contains("impact: HIGH"));
978 assert!(result.contains("description: A test skill"));
979 assert!(result.contains("tags:"));
980 assert!(result.contains("- test"));
981 assert!(result.contains("- example"));
982 assert!(result.contains("# Content here"));
983 }
984
985 #[test]
986 fn test_update_existing_inclusion_field() {
987 let content = r#"---
988title: My Skill
989inclusion: manual
990---
991
992# Content"#;
993
994 let result = add_auto_inclusion_frontmatter(content).unwrap();
995
996 assert!(result.contains("inclusion: auto"));
997 assert!(!result.contains("inclusion: manual"));
998 assert!(result.contains("title: My Skill"));
999 }
1000
1001 #[test]
1002 fn test_handle_frontmatter_with_complex_yaml() {
1003 let content = r#"---
1004title: Complex Skill
1005nested:
1006 field1: value1
1007 field2: value2
1008list:
1009 - item1
1010 - item2
1011 - item3
1012---
1013
1014# Content"#;
1015
1016 let result = add_auto_inclusion_frontmatter(content).unwrap();
1017
1018 assert!(result.contains("inclusion: auto"));
1019 assert!(result.contains("title: Complex Skill"));
1020 assert!(result.contains("nested:"));
1021 assert!(result.contains("field1: value1"));
1022 assert!(result.contains("list:"));
1023 assert!(result.contains("- item1"));
1024 }
1025
1026 #[test]
1027 fn test_malformed_frontmatter_missing_closing_delimiter() {
1028 let content = r#"---
1029title: My Skill
1030tags:
1031 - test
1032
1033# Content without closing ---"#;
1034
1035 let result = add_auto_inclusion_frontmatter(content);
1036
1037 assert!(result.is_err());
1038 match result {
1039 Err(SkillsError::YamlError(msg)) => {
1040 assert!(msg.contains("missing closing"));
1041 }
1042 _ => panic!("Expected YamlError"),
1043 }
1044 }
1045
1046 #[test]
1047 fn test_malformed_yaml_syntax() {
1048 let content = r#"---
1049title: My Skill
1050invalid: yaml: syntax:
1051---
1052
1053# Content"#;
1054
1055 let result = add_auto_inclusion_frontmatter(content);
1056
1057 assert!(result.is_err());
1058 match result {
1059 Err(SkillsError::YamlError(_)) => (),
1060 _ => panic!("Expected YamlError"),
1061 }
1062 }
1063
1064 #[test]
1065 fn test_frontmatter_with_special_characters() {
1066 let content = r#"---
1067title: "Skill with: special characters"
1068description: "Contains \"quotes\" and 'apostrophes'"
1069---
1070
1071# Content"#;
1072
1073 let result = add_auto_inclusion_frontmatter(content).unwrap();
1074
1075 assert!(result.contains("inclusion: auto"));
1076 assert!(result.contains("title:"));
1077 assert!(result.contains("description:"));
1078 }
1079
1080 #[test]
1081 fn test_frontmatter_with_multiline_strings() {
1082 let content = r#"---
1083title: My Skill
1084description: |
1085 This is a multiline
1086 description that spans
1087 multiple lines
1088---
1089
1090# Content"#;
1091
1092 let result = add_auto_inclusion_frontmatter(content).unwrap();
1093
1094 assert!(result.contains("inclusion: auto"));
1095 assert!(result.contains("title: My Skill"));
1096 assert!(result.contains("description:"));
1097 }
1098
1099 #[test]
1100 fn test_preserve_body_formatting() {
1101 let content = r#"---
1102title: My Skill
1103---
1104
1105# Heading 1
1106
1107Some paragraph with **bold** and *italic*.
1108
1109## Heading 2
1110
1111- List item 1
1112- List item 2
1113
1114```rust
1115fn example() {
1116 println!("code block");
1117}
1118```"#;
1119
1120 let result = add_auto_inclusion_frontmatter(content).unwrap();
1121
1122 assert!(result.contains("inclusion: auto"));
1123 assert!(result.contains("# Heading 1"));
1124 assert!(result.contains("**bold**"));
1125 assert!(result.contains("*italic*"));
1126 assert!(result.contains("## Heading 2"));
1127 assert!(result.contains("- List item 1"));
1128 assert!(result.contains("```rust"));
1129 assert!(result.contains("fn example()"));
1130 }
1131
1132 #[test]
1133 fn test_empty_frontmatter() {
1134 let content = r#"---
1135---
1136
1137# Content"#;
1138
1139 let result = add_auto_inclusion_frontmatter(content).unwrap();
1140
1141 assert!(result.contains("inclusion: auto"));
1142 assert!(result.contains("# Content"));
1143 }
1144
1145 #[test]
1146 fn test_frontmatter_with_only_inclusion() {
1147 let content = r#"---
1148inclusion: manual
1149---
1150
1151# Content"#;
1152
1153 let result = add_auto_inclusion_frontmatter(content).unwrap();
1154
1155 assert!(result.contains("inclusion: auto"));
1156 assert!(!result.contains("inclusion: manual"));
1157 }
1158
1159 #[test]
1160 fn test_frontmatter_with_numeric_values() {
1161 let content = r#"---
1162title: My Skill
1163priority: 1
1164version: 2.5
1165enabled: true
1166---
1167
1168# Content"#;
1169
1170 let result = add_auto_inclusion_frontmatter(content).unwrap();
1171
1172 assert!(result.contains("inclusion: auto"));
1173 assert!(result.contains("title: My Skill"));
1174 assert!(result.contains("priority:"));
1175 assert!(result.contains("version:"));
1176 assert!(result.contains("enabled:"));
1177 }
1178
1179 #[test]
1180 fn test_frontmatter_with_null_values() {
1181 let content = r#"---
1182title: My Skill
1183optional_field: null
1184---
1185
1186# Content"#;
1187
1188 let result = add_auto_inclusion_frontmatter(content).unwrap();
1189
1190 assert!(result.contains("inclusion: auto"));
1191 assert!(result.contains("title: My Skill"));
1192 }
1193
1194 #[test]
1195 fn test_content_starting_with_dashes_but_not_frontmatter() {
1196 let content = "--- This is not frontmatter\n\nJust regular content.";
1199 let result = add_auto_inclusion_frontmatter(content);
1200
1201 assert!(result.is_err());
1203 match result {
1204 Err(SkillsError::YamlError(msg)) => {
1205 assert!(msg.contains("missing closing"));
1206 }
1207 _ => panic!("Expected YamlError"),
1208 }
1209 }
1210
1211 #[test]
1212 fn test_frontmatter_preserves_field_order() {
1213 let content = r#"---
1214title: My Skill
1215impact: HIGH
1216description: Test
1217tags:
1218 - tag1
1219---
1220
1221# Content"#;
1222
1223 let result = add_auto_inclusion_frontmatter(content).unwrap();
1224
1225 assert!(result.contains("inclusion: auto"));
1227 assert!(result.contains("title:"));
1228 assert!(result.contains("impact:"));
1229 assert!(result.contains("description:"));
1230 assert!(result.contains("tags:"));
1231 }
1232}
1233
1234