use super::*;
use crate::config::LintConfig;
use crate::fs::RealFileSystem;
use crate::schemas::skill::{VALID_EFFORT_LEVELS, VALID_MODEL_ALIASES};
use std::fs;
#[test]
fn test_valid_skill() {
let content = r#"---
name: test-skill
description: Use when testing skill validation
---
Skill body content"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
assert!(diagnostics.is_empty());
}
#[test]
fn test_invalid_skill_name() {
let content = r#"---
name: Test-Skill
description: Use when validating skill names
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_004_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-004").collect();
assert_eq!(as_004_errors.len(), 1);
}
#[test]
fn test_as_001_missing_frontmatter() {
let content = include_str!("../../../../../tests/fixtures/skills/missing-frontmatter/SKILL.md");
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("SKILL.md"), content, &LintConfig::default());
let as_001_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-001").collect();
assert_eq!(as_001_errors.len(), 1);
}
#[test]
fn test_as_002_missing_name() {
let content = r#"---
description: Use when validating missing name
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_002_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-002").collect();
assert_eq!(as_002_errors.len(), 1);
}
#[test]
fn test_as_003_missing_description() {
let content = r#"---
name: test-skill
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_003_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-003").collect();
assert_eq!(as_003_errors.len(), 1);
}
#[test]
fn test_as_004_invalid_name_format() {
let content = r#"---
name: bad_name
description: Use when validating name format
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_004_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-004").collect();
assert_eq!(as_004_errors.len(), 1);
}
#[test]
fn test_as_007_reserved_name() {
let content = r#"---
name: claude
description: Use when validating reserved names
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_007_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-007").collect();
assert_eq!(as_007_errors.len(), 1);
}
#[test]
fn test_as_017_name_directory_mismatch() {
let content = r#"---
name: deploy-skill
description: Use when validating directory name matching
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(
Path::new("code-review/SKILL.md"),
content,
&LintConfig::default(),
);
let as_017_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-017").collect();
assert_eq!(as_017_errors.len(), 1);
}
#[test]
fn test_as_017_name_directory_match_ok() {
let content = r#"---
name: code-review
description: Use when validating directory name matching
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(
Path::new("code-review/SKILL.md"),
content,
&LintConfig::default(),
);
let as_017_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-017").collect();
assert_eq!(as_017_errors.len(), 0);
}
#[test]
fn test_as_018_description_first_second_person() {
let content = r#"---
name: review-skill
description: You can use this when reviewing pull requests
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_018_warnings: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-018").collect();
assert_eq!(as_018_warnings.len(), 1);
assert_eq!(
as_018_warnings[0].level,
crate::diagnostics::DiagnosticLevel::Warning
);
}
#[test]
fn test_as_018_description_third_person_ok() {
let content = r#"---
name: review-skill
description: Use when reviewing pull requests for quality issues
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_018_warnings: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-018").collect();
assert_eq!(as_018_warnings.len(), 0);
}
#[test]
fn test_as_019_vague_name() {
let content = r#"---
name: helper
description: Use when running helper tasks
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_019_warnings: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-019").collect();
assert_eq!(as_019_warnings.len(), 1);
}
#[test]
fn test_as_019_specific_name_ok() {
let content = r#"---
name: code-review-helper
description: Use when reviewing code quality
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_019_warnings: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-019").collect();
assert_eq!(as_019_warnings.len(), 0);
}
#[test]
fn test_as_008_description_too_long() {
let long_description = "a".repeat(1025);
let content = format!(
"---\nname: test-skill\ndescription: {}\n---\nBody",
long_description
);
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), &content, &LintConfig::default());
let as_008_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-008").collect();
assert_eq!(as_008_errors.len(), 1);
}
#[test]
fn test_as_008_description_empty_string() {
let content = r#"---
name: test-skill
description: ""
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_003_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-003").collect();
assert_eq!(as_003_errors.len(), 0);
let as_008_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-008").collect();
assert_eq!(as_008_errors.len(), 1);
}
#[test]
fn test_as_009_description_contains_xml() {
let content = r#"---
name: test-skill
description: Use when validating <xml> tags
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_009_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-009").collect();
assert_eq!(as_009_errors.len(), 1);
}
#[test]
fn test_as_011_compatibility_too_long() {
let long_compat = "b".repeat(501);
let content = format!(
"---\nname: test-skill\ndescription: Use when validating compatibility\ncompatibility: {}\n---\nBody",
long_compat
);
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), &content, &LintConfig::default());
let as_011_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-011").collect();
assert_eq!(as_011_errors.len(), 1);
}
#[test]
fn test_as_012_content_too_long() {
let body = (0..501).map(|_| "line").collect::<Vec<_>>().join("\n");
let content = format!(
"---\nname: test-skill\ndescription: Use when validating content length\n---\n{}",
body
);
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), &content, &LintConfig::default());
let as_012_warnings: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-012").collect();
assert_eq!(as_012_warnings.len(), 1);
}
#[test]
fn test_as_013_reference_too_deep() {
let content = include_str!("../../../../../tests/fixtures/skills/deep-reference/SKILL.md");
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("SKILL.md"), content, &LintConfig::default());
let as_013_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-013").collect();
assert_eq!(as_013_errors.len(), 1);
}
#[test]
fn test_as_013_reference_single_name_too_deep() {
let content = r#"---
name: deep-reference
description: Use when validating deep references
---
See reference/deep/guide.md for details."#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("SKILL.md"), content, &LintConfig::default());
let as_013_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-013").collect();
assert_eq!(as_013_errors.len(), 1);
}
#[test]
fn test_as_014_windows_path_separator() {
let content = include_str!("../../../../../tests/fixtures/skills/windows-path/SKILL.md");
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("SKILL.md"), content, &LintConfig::default());
let as_014_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-014").collect();
assert_eq!(as_014_errors.len(), 1);
}
#[test]
fn test_as_015_directory_size_exceeds() {
use std::io::Write;
let temp_dir = tempfile::tempdir().unwrap();
let skill_dir = temp_dir.path().join("big-skill");
fs::create_dir_all(&skill_dir).unwrap();
let skill_path = skill_dir.join("SKILL.md");
let mut skill_file = fs::File::create(&skill_path).unwrap();
writeln!(
skill_file,
"---\nname: big-skill\ndescription: Use when validating directory size\n---\nBody"
)
.unwrap();
let big_file_path = skill_dir.join("big.bin");
let big_payload = vec![0u8; 8 * 1024 * 1024 + 1];
fs::write(&big_file_path, big_payload).unwrap();
let content = fs::read_to_string(&skill_path).unwrap();
let validator = SkillValidator;
let diagnostics = validator.validate(&skill_path, &content, &LintConfig::default());
let as_015_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-015").collect();
assert_eq!(as_015_errors.len(), 1);
}
#[test]
fn test_cc_sk_006_dangerous_name_without_safety() {
let content = r#"---
name: deploy-prod
description: Deploys to production
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_006_errors: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-006")
.collect();
assert_eq!(cc_sk_006_errors.len(), 1);
assert_eq!(
cc_sk_006_errors[0].level,
crate::diagnostics::DiagnosticLevel::Error
);
}
#[test]
fn test_cc_sk_006_dangerous_name_with_safety() {
let content = r#"---
name: deploy-prod
description: Deploys to production
disable-model-invocation: true
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_006_errors: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-006")
.collect();
assert_eq!(cc_sk_006_errors.len(), 0);
}
#[test]
fn test_cc_sk_006_covers_all_dangerous_names() {
let dangerous_names = vec!["deploy", "ship", "publish", "delete", "release", "push"];
for name in dangerous_names {
let content = format!(
r#"---
name: {}-prod
description: A dangerous skill
---
Body"#,
name
);
let validator = SkillValidator;
let diagnostics =
validator.validate(Path::new("test.md"), &content, &LintConfig::default());
let cc_sk_006_errors: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-006")
.collect();
assert_eq!(
cc_sk_006_errors.len(),
1,
"Expected CC-SK-006 error for name: {}",
name
);
}
}
#[test]
fn test_cc_sk_007_unrestricted_bash() {
let content = r#"---
name: git-helper
description: Git operations helper
allowed-tools: Bash Read Write
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_007_warnings: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-007")
.collect();
assert_eq!(cc_sk_007_warnings.len(), 1);
assert_eq!(
cc_sk_007_warnings[0].level,
crate::diagnostics::DiagnosticLevel::Warning
);
}
#[test]
fn test_cc_sk_007_scoped_bash_ok() {
let content = r#"---
name: git-helper
description: Git operations helper
allowed-tools: Bash(git:*) Read Write
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_007_warnings: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-007")
.collect();
assert_eq!(cc_sk_007_warnings.len(), 0);
}
#[test]
fn test_cc_sk_007_no_bash() {
let content = r#"---
name: reader
description: File reader
allowed-tools: Read Write
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_007_warnings: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-007")
.collect();
assert_eq!(cc_sk_007_warnings.len(), 0);
}
#[test]
fn test_cc_sk_007_has_fix() {
let content = r#"---
name: git-helper
description: Use when doing git operations
allowed-tools: Bash Read Write
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_007: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-007")
.collect();
assert_eq!(cc_sk_007.len(), 1);
assert!(cc_sk_007[0].has_fixes());
let fix = &cc_sk_007[0].fixes[0];
assert_eq!(fix.replacement, "Bash(git:*)");
assert!(!fix.safe); }
#[test]
fn test_cc_sk_007_fix_correct_byte_position() {
let content = r#"---
name: helper
description: Use when helping
allowed-tools: Bash Read
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_007: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-007")
.collect();
assert_eq!(cc_sk_007.len(), 1);
assert!(cc_sk_007[0].has_fixes());
let fix = &cc_sk_007[0].fixes[0];
let mut fixed = content.to_string();
fixed.replace_range(fix.start_byte..fix.end_byte, &fix.replacement);
assert!(fixed.contains("Bash(git:*)"));
assert!(!fixed.contains("allowed-tools: Bash "));
}
#[test]
fn test_cc_sk_007_multiple_bash_multiple_fixes() {
let content = r#"---
name: helper
description: Use when helping
allowed-tools: Bash Read Bash
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_007: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-007")
.collect();
assert_eq!(cc_sk_007.len(), 2);
assert!(cc_sk_007[0].has_fixes());
assert!(cc_sk_007[1].has_fixes());
}
#[test]
fn test_cc_sk_007_scoped_bash_no_fix() {
let content = r#"---
name: helper
description: Use when helping
allowed-tools: Bash(git:*) Read
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_007: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-007")
.collect();
assert_eq!(cc_sk_007.len(), 0);
}
#[test]
fn test_find_plain_bash_positions() {
let content = "allowed-tools: Bash Read Bash(git:*) Write Bash";
let positions = find_plain_bash_positions(content, 0);
assert_eq!(positions.len(), 2);
assert_eq!(&content[positions[0].0..positions[0].1], "Bash");
assert_eq!(&content[positions[1].0..positions[1].1], "Bash");
}
#[test]
fn test_find_plain_bash_positions_none() {
let content = "allowed-tools: Bash(git:*) Bash(npm:*) Read";
let positions = find_plain_bash_positions(content, 0);
assert_eq!(positions.len(), 0);
}
#[test]
fn test_as_005_leading_hyphen() {
let content = r#"---
name: -bad-name
description: Use when testing validation
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_005_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-005").collect();
assert_eq!(as_005_errors.len(), 1);
assert_eq!(
as_005_errors[0].level,
crate::diagnostics::DiagnosticLevel::Error
);
}
#[test]
fn test_as_005_trailing_hyphen() {
let content = r#"---
name: bad-name-
description: Use when testing validation
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_005_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-005").collect();
assert_eq!(as_005_errors.len(), 1);
assert_eq!(
as_005_errors[0].level,
crate::diagnostics::DiagnosticLevel::Error
);
}
#[test]
fn test_as_006_consecutive_hyphens() {
let content = r#"---
name: bad--name
description: Use when testing validation
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_006_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-006").collect();
assert_eq!(as_006_errors.len(), 1);
assert_eq!(
as_006_errors[0].level,
crate::diagnostics::DiagnosticLevel::Error
);
}
#[test]
fn test_as_010_missing_trigger() {
let content = r#"---
name: code-review
description: Reviews code for quality
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_010_warnings: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-010").collect();
assert_eq!(as_010_warnings.len(), 1);
assert_eq!(
as_010_warnings[0].level,
crate::diagnostics::DiagnosticLevel::Warning
);
}
#[test]
fn test_as_010_has_use_when_trigger() {
let content = r#"---
name: code-review
description: Use when user asks for code review
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_010_warnings: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-010").collect();
assert_eq!(as_010_warnings.len(), 0);
}
#[test]
fn test_as_010_use_this_not_accepted() {
let content = r#"---
name: code-review
description: Use this skill to review code
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_010_warnings: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-010").collect();
assert_eq!(as_010_warnings.len(), 1);
}
#[test]
fn test_cc_sk_001_invalid_model() {
let content = r#"---
name: test-skill
description: Use when testing
model: gpt-4
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_001: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-001")
.collect();
assert_eq!(cc_sk_001.len(), 1);
assert_eq!(
cc_sk_001[0].level,
crate::diagnostics::DiagnosticLevel::Error
);
assert!(cc_sk_001[0].message.contains("gpt-4"));
}
#[test]
fn test_cc_sk_001_valid_models() {
for model in &["sonnet", "opus", "haiku", "inherit"] {
let content = format!(
r#"---
name: test-skill
description: Use when testing
model: {}
---
Body"#,
model
);
let validator = SkillValidator;
let diagnostics =
validator.validate(Path::new("test.md"), &content, &LintConfig::default());
let cc_sk_001: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-001")
.collect();
assert_eq!(cc_sk_001.len(), 0, "Model '{}' should be valid", model);
}
}
#[test]
fn test_cc_sk_001_no_model_ok() {
let content = r#"---
name: test-skill
description: Use when testing
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_001: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-001")
.collect();
assert_eq!(cc_sk_001.len(), 0);
}
#[test]
fn test_cc_sk_002_invalid_context() {
let content = r#"---
name: test-skill
description: Use when testing
context: split
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_002: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-002")
.collect();
assert_eq!(cc_sk_002.len(), 1);
assert_eq!(
cc_sk_002[0].level,
crate::diagnostics::DiagnosticLevel::Error
);
assert!(cc_sk_002[0].message.contains("split"));
}
#[test]
fn test_cc_sk_002_valid_context_fork() {
let content = r#"---
name: test-skill
description: Use when testing
context: fork
agent: general-purpose
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_002: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-002")
.collect();
assert_eq!(cc_sk_002.len(), 0);
}
#[test]
fn test_cc_sk_002_no_context_ok() {
let content = r#"---
name: test-skill
description: Use when testing
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_002: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-002")
.collect();
assert_eq!(cc_sk_002.len(), 0);
}
#[test]
fn test_cc_sk_003_context_fork_without_agent() {
let content = r#"---
name: test-skill
description: Use when testing
context: fork
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_003: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-003")
.collect();
assert_eq!(cc_sk_003.len(), 1);
assert_eq!(
cc_sk_003[0].level,
crate::diagnostics::DiagnosticLevel::Error
);
}
#[test]
fn test_cc_sk_003_context_fork_with_agent_ok() {
let content = r#"---
name: test-skill
description: Use when testing
context: fork
agent: Explore
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_003: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-003")
.collect();
assert_eq!(cc_sk_003.len(), 0);
}
#[test]
fn test_cc_sk_004_agent_without_context() {
let content = r#"---
name: test-skill
description: Use when testing
agent: Explore
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_004: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-004")
.collect();
assert_eq!(cc_sk_004.len(), 1);
assert_eq!(
cc_sk_004[0].level,
crate::diagnostics::DiagnosticLevel::Error
);
}
#[test]
fn test_cc_sk_004_agent_with_context_ok() {
let content = r#"---
name: test-skill
description: Use when testing
context: fork
agent: Explore
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_004: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-004")
.collect();
assert_eq!(cc_sk_004.len(), 0);
}
#[test]
fn test_cc_sk_004_no_agent_no_context_ok() {
let content = r#"---
name: test-skill
description: Use when testing
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_004: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-004")
.collect();
assert_eq!(cc_sk_004.len(), 0);
}
#[test]
fn test_cc_sk_005_invalid_agent() {
let content = r#"---
name: test-skill
description: Use when testing
context: fork
agent: CustomAgent
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_005: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-005")
.collect();
assert_eq!(cc_sk_005.len(), 1);
assert_eq!(
cc_sk_005[0].level,
crate::diagnostics::DiagnosticLevel::Error
);
assert!(cc_sk_005[0].message.contains("CustomAgent"));
}
#[test]
fn test_cc_sk_005_valid_agents() {
for agent in &["Explore", "Plan", "general-purpose"] {
let content = format!(
r#"---
name: test-skill
description: Use when testing
context: fork
agent: {}
---
Body"#,
agent
);
let validator = SkillValidator;
let diagnostics =
validator.validate(Path::new("test.md"), &content, &LintConfig::default());
let cc_sk_005: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-005")
.collect();
assert_eq!(cc_sk_005.len(), 0, "Agent '{}' should be valid", agent);
}
}
#[test]
fn test_cc_sk_005_valid_custom_agents() {
for agent in &[
"my-custom-agent",
"code-review",
"deploy-helper",
"a",
"agent123",
"my-agent-v2",
] {
let content = format!(
r#"---
name: test-skill
description: Use when testing
context: fork
agent: {}
---
Body"#,
agent
);
let validator = SkillValidator;
let diagnostics =
validator.validate(Path::new("test.md"), &content, &LintConfig::default());
let cc_sk_005: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-005")
.collect();
assert_eq!(
cc_sk_005.len(),
0,
"Custom agent '{}' should be valid",
agent
);
}
}
#[test]
fn test_cc_sk_005_rejects_invalid_agent_formats() {
let invalid_agents = [
("MyAgent", "uppercase"),
("my_custom_agent", "underscore"),
("\"\"", "empty"),
("-custom-agent", "leading hyphen"),
("custom-agent-", "trailing hyphen"),
("custom--agent", "consecutive hyphens"),
("my@agent", "special char @"),
("agent!", "special char !"),
("test.agent", "special char ."),
("agent/name", "special char /"),
];
for (agent, reason) in invalid_agents {
let content = format!(
r#"---
name: test-skill
description: Use when testing
context: fork
agent: {}
---
Body"#,
agent
);
let validator = SkillValidator;
let diagnostics =
validator.validate(Path::new("test.md"), &content, &LintConfig::default());
let cc_sk_005: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-005")
.collect();
assert_eq!(
cc_sk_005.len(),
1,
"Agent '{}' ({}) should be rejected",
agent,
reason
);
}
}
#[test]
fn test_cc_sk_005_rejects_too_long_agent() {
let long_agent = "a".repeat(65);
let content = format!(
r#"---
name: test-skill
description: Use when testing
context: fork
agent: {}
---
Body"#,
long_agent
);
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), &content, &LintConfig::default());
let cc_sk_005: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-005")
.collect();
assert_eq!(cc_sk_005.len(), 1, "Agent over 64 chars should be rejected");
}
#[test]
fn test_cc_sk_005_accepts_max_length_agent() {
let max_agent = "a".repeat(64);
let content = format!(
r#"---
name: test-skill
description: Use when testing
context: fork
agent: {}
---
Body"#,
max_agent
);
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), &content, &LintConfig::default());
let cc_sk_005: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-005")
.collect();
assert_eq!(cc_sk_005.len(), 0, "Agent at 64 chars should be accepted");
}
#[test]
fn test_cc_sk_005_fixture_invalid_agent() {
let content =
include_str!("../../../../../tests/fixtures/invalid/skills/invalid-agent/SKILL.md");
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("SKILL.md"), content, &LintConfig::default());
let cc_sk_005: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-005")
.collect();
assert_eq!(
cc_sk_005.len(),
1,
"Invalid agent fixture should trigger CC-SK-005"
);
}
#[test]
fn test_cc_sk_005_fixture_valid_custom_agent() {
let content =
include_str!("../../../../../tests/fixtures/valid/skills/with-custom-agent/SKILL.md");
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("SKILL.md"), content, &LintConfig::default());
let cc_sk_005: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-005")
.collect();
assert_eq!(
cc_sk_005.len(),
0,
"Valid custom agent fixture should pass CC-SK-005"
);
}
#[test]
fn test_cc_sk_008_unknown_tool() {
let content = r#"---
name: test-skill
description: Use when testing
allowed-tools: Read Write UnknownTool
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_008: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-008")
.collect();
assert_eq!(cc_sk_008.len(), 1);
assert_eq!(
cc_sk_008[0].level,
crate::diagnostics::DiagnosticLevel::Error
);
assert!(cc_sk_008[0].message.contains("UnknownTool"));
}
#[test]
fn test_cc_sk_008_all_known_tools_ok() {
let content = r#"---
name: test-skill
description: Use when testing
allowed-tools: Bash Read Write Edit Grep Glob Task WebFetch WebSearch AskUserQuestion TodoRead TodoWrite MultiTool NotebookEdit EnterPlanMode ExitPlanMode Skill StatusBarMessageTool SendMessageTool TaskOutput
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_008: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-008")
.collect();
assert_eq!(cc_sk_008.len(), 0);
}
#[test]
fn test_cc_sk_008_scoped_tool_extracts_base_name() {
let content = r#"---
name: test-skill
description: Use when testing
allowed-tools: Bash(git:*) Read Write
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_008: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-008")
.collect();
assert_eq!(cc_sk_008.len(), 0);
}
#[test]
fn test_cc_sk_008_multiple_unknown_tools() {
let content = r#"---
name: test-skill
description: Use when testing
allowed-tools: FakeTool1 Read FakeTool2
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_008: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-008")
.collect();
assert_eq!(cc_sk_008.len(), 2);
}
#[test]
fn test_cc_sk_008_scoped_unknown_tool() {
let content = r#"---
name: test-skill
description: Use when testing
allowed-tools: FakeTool(scope:*) Read
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_008: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-008")
.collect();
assert_eq!(
cc_sk_008.len(),
1,
"Should detect FakeTool as unknown even when scoped"
);
assert!(cc_sk_008[0].message.contains("FakeTool"));
}
#[test]
fn test_cc_sk_009_too_many_injections() {
let content = r#"---
name: test-skill
description: Use when testing
---
Current date: !`date`
Git status: !`git status`
Branch: !`git branch`
User: !`whoami`
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_009: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-009")
.collect();
assert_eq!(cc_sk_009.len(), 1);
assert_eq!(
cc_sk_009[0].level,
crate::diagnostics::DiagnosticLevel::Warning
);
assert!(cc_sk_009[0].message.contains("4"));
}
#[test]
fn test_cc_sk_009_exactly_three_injections_ok() {
let content = r#"---
name: test-skill
description: Use when testing
---
Date: !`date`
Status: !`git status`
Branch: !`git branch`
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_009: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-009")
.collect();
assert_eq!(cc_sk_009.len(), 0);
}
#[test]
fn test_cc_sk_009_no_injections_ok() {
let content = r#"---
name: test-skill
description: Use when testing
---
No dynamic injections here.
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_009: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-009")
.collect();
assert_eq!(cc_sk_009.len(), 0);
}
#[test]
fn test_cc_sk_006_explicit_false_still_triggers() {
let content = r#"---
name: deploy-prod
description: Use when deploying
disable-model-invocation: false
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_006: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-006")
.collect();
assert_eq!(
cc_sk_006.len(),
1,
"Explicit false should still trigger CC-SK-006"
);
}
#[test]
fn test_cc_sk_007_duplicate_bash_multiple_warnings() {
let content = r#"---
name: test-skill
description: Use when testing
allowed-tools: Bash Read Bash
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_007: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-007")
.collect();
assert_eq!(
cc_sk_007.len(),
2,
"Each Bash occurrence triggers a warning"
);
}
#[test]
fn test_cc_sk_008_malformed_scope_no_panic() {
let content = r#"---
name: test-skill
description: Use when testing
allowed-tools: Bash( Read Bash() Write
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_008: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-008")
.collect();
assert_eq!(
cc_sk_008.len(),
0,
"Malformed scopes should extract base name correctly"
);
}
#[test]
fn test_cc_sk_008_lowercase_tool_unknown() {
let content = r#"---
name: test-skill
description: Use when testing
allowed-tools: bash read
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_008: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-008")
.collect();
assert_eq!(cc_sk_008.len(), 2, "lowercase tool names are unknown");
}
#[test]
fn test_cc_sk_008_mcp_tool_valid() {
let content = r#"---
name: test-skill
description: Use when testing
allowed-tools: Read mcp__memory__create_entities mcp__filesystem__read_file
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_008: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-008")
.collect();
assert_eq!(
cc_sk_008.len(),
0,
"MCP tools with mcp__ prefix should be accepted in allowed-tools"
);
}
#[test]
fn test_cc_sk_008_scoped_mcp_tool_valid() {
let content = r#"---
name: test-skill
description: Use when testing
allowed-tools: Read mcp__github__search_repositories(scope:*)
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_008: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-008")
.collect();
assert_eq!(
cc_sk_008.len(),
0,
"Scoped MCP tools should be accepted in allowed-tools"
);
}
#[test]
fn test_cc_sk_008_mcp_case_sensitive() {
let content = r#"---
name: test-skill
description: Use when testing
allowed-tools: MCP__memory__create Mcp__test__tool
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_008: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-008")
.collect();
assert_eq!(
cc_sk_008.len(),
2,
"MCP prefix is case-sensitive: MCP__ and Mcp__ should be rejected"
);
}
#[test]
fn test_as_010_case_insensitive() {
let content = r#"---
name: test-skill
description: USE WHEN testing
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_010: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-010").collect();
assert_eq!(
as_010.len(),
0,
"'USE WHEN' should match case-insensitively"
);
}
#[test]
fn test_parse_error_handling() {
let content = r#"---
name: test
description
invalid yaml
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let parse_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-016").collect();
assert_eq!(
parse_errors.len(),
1,
"Invalid YAML should produce parse error"
);
}
#[test]
fn test_config_disabled_skills_category() {
let mut config = LintConfig::default();
config.rules_mut().skills = false;
let content = r#"---
name: -bad-name
description: Missing trigger phrase
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &config);
let skill_rules: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule.starts_with("AS-") || d.rule.starts_with("CC-SK-"))
.collect();
assert_eq!(skill_rules.len(), 0);
}
#[test]
fn test_config_disabled_specific_skill_rule() {
let mut config = LintConfig::default();
config.rules_mut().disabled_rules = vec!["AS-005".to_string()];
let content = r#"---
name: -bad-name
description: Missing trigger phrase
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &config);
let as_005: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-005").collect();
assert_eq!(as_005.len(), 0);
let as_010: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-010").collect();
assert_eq!(as_010.len(), 1);
}
#[test]
fn test_config_cursor_target_disables_cc_sk_rules() {
use crate::config::TargetTool;
let mut config = LintConfig::default();
config.set_target(TargetTool::Cursor);
let content = r#"---
name: deploy-prod
description: Deploys to production
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &config);
let cc_sk_006: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-006")
.collect();
assert_eq!(cc_sk_006.len(), 0);
let as_010: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-010").collect();
assert_eq!(as_010.len(), 1);
}
#[test]
fn test_config_claude_code_target_enables_cc_sk_rules() {
use crate::config::TargetTool;
let mut config = LintConfig::default();
config.set_target(TargetTool::ClaudeCode);
let content = r#"---
name: deploy-prod
description: Use when deploying to production
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &config);
let cc_sk_006: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-006")
.collect();
assert_eq!(cc_sk_006.len(), 1);
}
#[test]
fn test_convert_to_kebab_case_lowercase() {
assert_eq!(convert_to_kebab_case("TestSkill"), "testskill");
}
#[test]
fn test_convert_to_kebab_case_underscores() {
assert_eq!(convert_to_kebab_case("test_skill"), "test-skill");
}
#[test]
fn test_convert_to_kebab_case_mixed() {
assert_eq!(convert_to_kebab_case("Test_Skill_Name"), "test-skill-name");
}
#[test]
fn test_convert_to_kebab_case_consecutive_hyphens() {
assert_eq!(convert_to_kebab_case("test--skill"), "test-skill");
assert_eq!(convert_to_kebab_case("test___skill"), "test-skill");
}
#[test]
fn test_convert_to_kebab_case_leading_trailing() {
assert_eq!(convert_to_kebab_case("-test-skill-"), "test-skill");
assert_eq!(convert_to_kebab_case("_test_skill_"), "test-skill");
}
#[test]
fn test_convert_to_kebab_case_invalid_chars() {
assert_eq!(convert_to_kebab_case("test@skill!"), "testskill");
assert_eq!(convert_to_kebab_case("test.skill"), "testskill");
}
#[test]
fn test_convert_to_kebab_case_truncate() {
let long_name = "a".repeat(100);
let result = convert_to_kebab_case(&long_name);
assert!(result.len() <= 64);
assert_eq!(result.len(), 64);
}
#[test]
fn test_as_004_has_fix() {
let content = r#"---
name: Test_Skill
description: Use when testing
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_004: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-004").collect();
assert_eq!(as_004.len(), 1);
assert!(as_004[0].has_fixes());
assert_eq!(as_004[0].fixes[0].replacement, "test-skill");
}
#[test]
fn test_as_004_fix_case_only_is_safe() {
let content = r#"---
name: TestSkill
description: Use when testing
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_004: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-004").collect();
assert_eq!(as_004.len(), 1);
assert!(as_004[0].has_fixes());
assert!(as_004[0].fixes[0].safe);
}
#[test]
fn test_as_004_fix_structural_is_unsafe() {
let content = r#"---
name: Test_Skill
description: Use when testing
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_004: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-004").collect();
assert_eq!(as_004.len(), 1);
assert!(as_004[0].has_fixes());
assert!(!as_004[0].fixes[0].safe);
}
#[test]
fn test_as_004_fix_byte_position() {
let content = r#"---
name: Bad_Name
description: Use when testing
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_004: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-004").collect();
assert_eq!(as_004.len(), 1);
assert!(as_004[0].has_fixes());
let fix = &as_004[0].fixes[0];
let mut fixed = content.to_string();
fixed.replace_range(fix.start_byte..fix.end_byte, &fix.replacement);
assert!(fixed.contains("name: bad-name"));
}
#[test]
fn test_as_004_fix_quoted_value() {
let content = r#"---
name: "Bad_Name"
description: Use when testing
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_004: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-004").collect();
assert_eq!(as_004.len(), 1);
assert!(as_004[0].has_fixes());
let fix = &as_004[0].fixes[0];
let mut fixed = content.to_string();
fixed.replace_range(fix.start_byte..fix.end_byte, &fix.replacement);
assert!(fixed.contains("bad-name"));
}
#[test]
fn test_as_004_no_fix_when_converts_to_empty() {
let content = r#"---
name: "!@#$%"
description: Use when testing
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_004: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-004").collect();
assert_eq!(as_004.len(), 1);
assert!(!as_004[0].has_fixes());
}
#[test]
fn test_as_004_underscore_to_hyphen_is_unsafe() {
let content = r#"---
name: test_name
description: Use when testing
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_004: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-004").collect();
assert_eq!(as_004.len(), 1);
assert!(as_004[0].has_fixes());
assert!(!as_004[0].fixes[0].safe);
}
#[test]
fn test_as_010_has_fix() {
let content = r#"---
name: code-review
description: Reviews code for quality
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_010: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-010").collect();
assert_eq!(as_010.len(), 1);
assert!(as_010[0].has_fixes());
assert_eq!(
as_010[0].fixes[0].replacement,
"Use when user wants to Reviews code for quality"
);
}
#[test]
fn test_as_010_fix_is_unsafe() {
let content = r#"---
name: code-review
description: Reviews code for quality
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_010: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-010").collect();
assert_eq!(as_010.len(), 1);
assert!(as_010[0].has_fixes());
assert!(!as_010[0].fixes[0].safe);
}
#[test]
fn test_as_010_fix_byte_position() {
let content = r#"---
name: helper
description: Helps with tasks
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_010: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-010").collect();
assert_eq!(as_010.len(), 1);
assert!(as_010[0].has_fixes());
let fix = &as_010[0].fixes[0];
let mut fixed = content.to_string();
fixed.replace_range(fix.start_byte..fix.end_byte, &fix.replacement);
assert!(fixed.contains("Use when user wants to Helps with tasks"));
}
#[test]
fn test_as_010_fix_quoted_value() {
let content = r#"---
name: helper
description: "Helps with tasks"
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_010: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-010").collect();
assert_eq!(as_010.len(), 1);
assert!(as_010[0].has_fixes());
let fix = &as_010[0].fixes[0];
let mut fixed = content.to_string();
fixed.replace_range(fix.start_byte..fix.end_byte, &fix.replacement);
assert!(fixed.contains("Use when user wants to Helps with tasks"));
}
#[test]
fn test_as_010_no_fix_when_description_too_long() {
let long_desc = "a".repeat(1010);
let content = format!("---\nname: helper\ndescription: {}\n---\nBody", long_desc);
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), &content, &LintConfig::default());
let as_010: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-010").collect();
assert_eq!(as_010.len(), 1);
assert!(!as_010[0].has_fixes());
}
#[test]
fn test_frontmatter_value_byte_range_unquoted() {
let content = r#"---
name: test-skill
description: A test skill
---
Body"#;
let parts = split_frontmatter(content);
let range = frontmatter_value_byte_range(content, &parts, "name");
assert!(range.is_some());
let (start, end) = range.unwrap();
assert_eq!(&content[start..end], "test-skill");
}
#[test]
fn test_frontmatter_value_byte_range_double_quoted() {
let content = r#"---
name: "test-skill"
description: A test skill
---
Body"#;
let parts = split_frontmatter(content);
let range = frontmatter_value_byte_range(content, &parts, "name");
assert!(range.is_some());
let (start, end) = range.unwrap();
assert_eq!(&content[start..end], "test-skill");
}
#[test]
fn test_frontmatter_value_byte_range_single_quoted() {
let content = r#"---
name: 'test-skill'
description: A test skill
---
Body"#;
let parts = split_frontmatter(content);
let range = frontmatter_value_byte_range(content, &parts, "name");
assert!(range.is_some());
let (start, end) = range.unwrap();
assert_eq!(&content[start..end], "test-skill");
}
#[test]
fn test_frontmatter_value_byte_range_not_found() {
let content = r#"---
name: test-skill
---
Body"#;
let parts = split_frontmatter(content);
let range = frontmatter_value_byte_range(content, &parts, "description");
assert!(range.is_none());
}
#[test]
fn test_frontmatter_value_byte_range_exhaustive() {
let content = "---
name: test-skill
description: \"Quoted description\"
empty:
nested: value
unquoted_with_comment: value # this is a comment
quoted_with_comment: \"quoted value\" # this is also a comment
single_quoted: 'single value'
with_colon: \"value: with colon\"
indented_key: indented_value
---
Body";
let parts = split_frontmatter(content);
let range = frontmatter_value_byte_range(content, &parts, "name");
assert_eq!(&content[range.unwrap().0..range.unwrap().1], "test-skill");
let range = frontmatter_value_byte_range(content, &parts, "description");
assert_eq!(
&content[range.unwrap().0..range.unwrap().1],
"Quoted description"
);
let range = frontmatter_value_byte_range(content, &parts, "empty");
assert!(range.is_none());
let range = frontmatter_value_byte_range(content, &parts, "unquoted_with_comment");
assert_eq!(&content[range.unwrap().0..range.unwrap().1], "value");
let range = frontmatter_value_byte_range(content, &parts, "quoted_with_comment");
assert_eq!(&content[range.unwrap().0..range.unwrap().1], "quoted value");
let range = frontmatter_value_byte_range(content, &parts, "single_quoted");
assert_eq!(&content[range.unwrap().0..range.unwrap().1], "single value");
let range = frontmatter_value_byte_range(content, &parts, "with_colon");
assert_eq!(
&content[range.unwrap().0..range.unwrap().1],
"value: with colon"
);
let range = frontmatter_value_byte_range(content, &parts, "indented_key");
assert_eq!(
&content[range.unwrap().0..range.unwrap().1],
"indented_value"
);
let content_crlf = "---\r\nname: test-skill\r\ndescription: value\r\n---\r\nBody";
let parts_crlf = split_frontmatter(content_crlf);
let range = frontmatter_value_byte_range(content_crlf, &parts_crlf, "name");
assert_eq!(
&content_crlf[range.unwrap().0..range.unwrap().1],
"test-skill"
);
let content_malformed = "---\nname: \"unclosed\ndescription: value\n---\nBody";
let parts_malformed = split_frontmatter(content_malformed);
let range = frontmatter_value_byte_range(content_malformed, &parts_malformed, "name");
assert!(range.is_none());
}
#[test]
fn test_key_helpers_exhaustive() {
let content = "---
name: test-skill
indented_key: value
# comment
other: val
---";
let parts = split_frontmatter(content);
assert_eq!(frontmatter_key_offset(&parts.frontmatter, "name"), Some(0));
assert_eq!(
frontmatter_key_offset(&parts.frontmatter, "indented_key"),
Some(19)
);
let range = frontmatter_key_line_byte_range(content, &parts, "name").unwrap();
assert_eq!(&content[range.0..range.1], "name: test-skill\n");
let range = frontmatter_key_line_byte_range(content, &parts, "other").unwrap();
assert_eq!(&content[range.0..range.1], "other: val\n");
let content_crlf = "---\r\nname: test\r\nother: val\r\n---";
let parts_crlf = split_frontmatter(content_crlf);
let range = frontmatter_key_line_byte_range(content_crlf, &parts_crlf, "name").unwrap();
assert_eq!(&content_crlf[range.0..range.1], "name: test\r\n");
}
fn write_bytes_to_file(path: &std::path::Path, num_bytes: usize) {
use std::io::Write;
let mut file = fs::File::create(path).expect("Failed to create test file");
let buffer = [0u8; 8192];
let mut remaining = num_bytes;
while remaining > 0 {
let to_write = remaining.min(buffer.len());
file.write_all(&buffer[..to_write])
.expect("Failed to write test data");
remaining -= to_write;
}
}
#[test]
fn test_directory_size_until_short_circuits() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let fs = RealFileSystem;
for i in 0..10 {
let file_path = temp_dir.path().join(format!("file_{:02}.bin", i));
write_bytes_to_file(&file_path, 1024 * 1024);
}
let size = directory_size_until(temp_dir.path(), 2 * 1024 * 1024, &fs);
assert!(size > 2 * 1024 * 1024, "Size should exceed 2MB limit");
assert!(
size <= 3 * 1024 * 1024,
"Size {} should be <= 3MB (short-circuited)",
size
);
}
#[test]
fn test_directory_size_until_accurate_under_limit() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let fs = RealFileSystem;
for i in 0..2 {
let file_path = temp_dir.path().join(format!("file_{}.bin", i));
write_bytes_to_file(&file_path, 1024);
}
let size = directory_size_until(temp_dir.path(), 1024 * 1024, &fs);
assert_eq!(size, 2048);
}
#[test]
fn test_directory_size_until_handles_empty_directory() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let fs = RealFileSystem;
let size = directory_size_until(temp_dir.path(), 1024 * 1024, &fs);
assert_eq!(size, 0);
}
#[test]
fn test_directory_size_until_nested_directories() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let real_fs = RealFileSystem;
let sub1 = temp_dir.path().join("sub1");
let sub2 = sub1.join("sub2");
fs::create_dir_all(&sub2).expect("Failed to create nested directories");
write_bytes_to_file(&temp_dir.path().join("root.bin"), 1024);
write_bytes_to_file(&sub1.join("sub1.bin"), 2048);
write_bytes_to_file(&sub2.join("sub2.bin"), 3072);
let size = directory_size_until(temp_dir.path(), 1024 * 1024, &real_fs);
assert_eq!(size, 6144, "Should sum files across all nested directories");
}
#[test]
fn test_directory_size_until_nested_short_circuits() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let real_fs = RealFileSystem;
let sub1 = temp_dir.path().join("sub1");
let sub2 = sub1.join("sub2");
fs::create_dir_all(&sub2).expect("Failed to create nested directories");
write_bytes_to_file(&temp_dir.path().join("root.bin"), 1024 * 1024);
write_bytes_to_file(&sub1.join("sub1.bin"), 1024 * 1024);
write_bytes_to_file(&sub2.join("sub2.bin"), 1024 * 1024);
let size = directory_size_until(temp_dir.path(), 2 * 1024 * 1024, &real_fs);
assert!(size > 2 * 1024 * 1024, "Should exceed limit");
assert!(
size <= 3 * 1024 * 1024,
"Should short-circuit before scanning all"
);
}
#[test]
fn test_as_015_boundary_exactly_8mb() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let skill_dir = temp_dir.path().join("skill");
fs::create_dir_all(&skill_dir).expect("Failed to create skill directory");
let skill_path = skill_dir.join("SKILL.md");
fs::write(&skill_path, "---\nname: boundary-test\n---\nBody")
.expect("Failed to write SKILL.md");
let skill_md_size = fs::metadata(&skill_path)
.expect("Failed to read SKILL.md metadata")
.len() as usize;
let target_size = 8 * 1024 * 1024 - skill_md_size;
write_bytes_to_file(&skill_dir.join("data.bin"), target_size);
let validator = SkillValidator;
let content = fs::read_to_string(&skill_path).expect("Failed to read SKILL.md content");
let diagnostics = validator.validate(&skill_path, &content, &LintConfig::default());
let as_015_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-015").collect();
assert!(
as_015_errors.is_empty(),
"Exactly 8MB should not trigger AS-015, but got: {:?}",
as_015_errors
);
}
#[test]
fn test_as_001_missing_closing_delimiter_treated_as_no_frontmatter() {
let content = r#"---
name: test
description: A test skill
Missing closing delimiter
Body content"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_001: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-001").collect();
assert_eq!(as_001.len(), 1);
let as_016: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-016").collect();
assert!(as_016.is_empty());
}
#[test]
fn test_as_016_invalid_yaml_colon_in_value() {
let content = r#"---
name: test:value:with:colons
description: A test skill
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let parse_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-016").collect();
assert!(parse_errors.is_empty());
}
#[test]
fn test_as_016_invalid_yaml_tabs() {
let content = "---\nname: test\n\tdescription: bad indent\n---\nBody";
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let parse_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-016").collect();
assert_eq!(
parse_errors.len(),
1,
"Tab indentation should cause parse error"
);
}
#[test]
fn test_as_016_valid_yaml_no_error() {
let content = r#"---
name: valid-skill
description: A properly formatted skill
model: sonnet
---
Body content"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let parse_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-016").collect();
assert!(parse_errors.is_empty());
}
#[test]
fn test_as_016_disabled() {
let mut config = LintConfig::default();
config.rules_mut().disabled_rules = vec!["AS-016".to_string()];
let content = r#"---
name: test
description
invalid yaml syntax
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &config);
assert!(!diagnostics.iter().any(|d| d.rule == "AS-016"));
}
#[test]
fn test_as_001_empty_file() {
let content = "";
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
assert!(diagnostics.iter().any(|d| d.rule == "AS-001"));
}
#[test]
fn test_as_001_only_body_no_frontmatter() {
let content = "This is just body content without any frontmatter.";
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
assert!(diagnostics.iter().any(|d| d.rule == "AS-001"));
}
#[test]
fn test_as_002_no_name_field() {
let content = r#"---
description: A test skill without name field
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
assert!(diagnostics.iter().any(|d| d.rule == "AS-002"));
}
#[test]
fn test_as_004_whitespace_only_name() {
let content = r#"---
name: " "
description: A test skill
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
assert!(diagnostics.iter().any(|d| d.rule == "AS-004"));
}
#[test]
fn test_as_003_whitespace_description() {
let content = r#"---
name: test-skill
description: " "
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_008: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-008").collect();
assert!(
!as_008.is_empty(),
"Whitespace description should trigger AS-008"
);
}
#[test]
fn test_as_004_uppercase_in_name() {
let content = r#"---
name: TestSkill
description: Use when testing skill names
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_004: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-004").collect();
assert_eq!(as_004.len(), 1);
}
#[test]
fn test_as_004_valid_lowercase_hyphen_name() {
let content = r#"---
name: valid-skill-name
description: Use when testing valid names
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
assert!(!diagnostics.iter().any(|d| d.rule == "AS-004"));
}
#[test]
fn test_as_007_all_reserved_names() {
let reserved = ["anthropic", "claude", "skill"];
for name in reserved {
let content = format!(
"---\nname: {}\ndescription: Use when testing reserved names\n---\nBody",
name
);
let validator = SkillValidator;
let diagnostics =
validator.validate(Path::new("test.md"), &content, &LintConfig::default());
let as_007: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-007").collect();
assert_eq!(
as_007.len(),
1,
"Reserved name '{}' should trigger AS-007",
name
);
}
}
#[test]
fn test_as_007_non_reserved_name_ok() {
let content = r#"---
name: my-custom-skill
description: Use when testing non-reserved names
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
assert!(!diagnostics.iter().any(|d| d.rule == "AS-007"));
}
#[test]
fn test_as_011_exactly_500_chars() {
let long_compat = "a".repeat(500);
let content = format!(
"---\nname: test\ndescription: Use when testing\ncompatibility: {}\n---\nBody",
long_compat
);
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), &content, &LintConfig::default());
assert!(!diagnostics.iter().any(|d| d.rule == "AS-011"));
}
#[test]
fn test_as_011_501_chars_triggers() {
let long_compat = "a".repeat(501);
let content = format!(
"---\nname: test\ndescription: Use when testing\ncompatibility: {}\n---\nBody",
long_compat
);
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), &content, &LintConfig::default());
assert!(diagnostics.iter().any(|d| d.rule == "AS-011"));
}
#[test]
fn test_as_012_exactly_500_lines_ok() {
let body_lines = (0..470)
.map(|i| format!("Line {}", i))
.collect::<Vec<_>>()
.join("\n");
let content = format!(
"---\nname: test\ndescription: Use when testing line limits\n---\n{}",
body_lines
);
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), &content, &LintConfig::default());
assert!(!diagnostics.iter().any(|d| d.rule == "AS-012"));
}
#[test]
fn test_cc_sk_001_all_valid_models() {
let valid_models = VALID_MODEL_ALIASES;
for model in valid_models {
let content = format!(
"---\nname: test\ndescription: Use when testing models\nmodel: {}\n---\nBody",
model
);
let validator = SkillValidator;
let diagnostics =
validator.validate(Path::new("test.md"), &content, &LintConfig::default());
let cc_sk_001: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-001")
.collect();
assert!(
cc_sk_001.is_empty(),
"Model '{}' should be valid but got CC-SK-001",
model
);
}
}
#[test]
fn test_cc_sk_001_invalid_model_exhaustive() {
let content = r#"---
name: test
description: Use when testing
model: invalid-model
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
assert!(diagnostics.iter().any(|d| d.rule == "CC-SK-001"));
}
#[test]
fn test_cc_sk_002_fork_context_valid() {
let content = r#"---
name: test
description: Use when testing contexts
context: fork
agent: general-purpose
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_002: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-002")
.collect();
assert!(
cc_sk_002.is_empty(),
"Context 'fork' with agent should be valid"
);
}
#[test]
fn test_cc_sk_002_invalid_context_exhaustive() {
let content = r#"---
name: test
description: Use when testing contexts
context: invalid
agent: general-purpose
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
assert!(diagnostics.iter().any(|d| d.rule == "CC-SK-002"));
}
#[test]
fn test_cc_sk_003_fork_without_agent_exhaustive() {
let content = r#"---
name: test
description: Use when testing
context: fork
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
assert!(diagnostics.iter().any(|d| d.rule == "CC-SK-003"));
}
#[test]
fn test_cc_sk_004_agent_without_context_exhaustive() {
let content = r#"---
name: test
description: Use when testing
agent: general-purpose
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
assert!(diagnostics.iter().any(|d| d.rule == "CC-SK-004"));
}
#[test]
fn test_cc_sk_005_builtin_agents_valid() {
let builtin_agents = BUILTIN_AGENTS;
for agent in builtin_agents {
let content = format!(
"---\nname: test\ndescription: Use when testing\ncontext: fork\nagent: {}\n---\nBody",
agent
);
let validator = SkillValidator;
let diagnostics =
validator.validate(Path::new("test.md"), &content, &LintConfig::default());
let cc_sk_005: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-005")
.collect();
assert!(
cc_sk_005.is_empty(),
"Built-in agent '{}' should be valid",
agent
);
}
}
#[test]
fn test_cc_sk_005_custom_kebab_agent_valid() {
let content = r#"---
name: test
description: Use when testing
context: fork
agent: my-custom-agent
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
assert!(!diagnostics.iter().any(|d| d.rule == "CC-SK-005"));
}
#[test]
fn test_as_005_has_safe_fix() {
let content = r#"---
name: -bad-name
description: Use when testing
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_005 = diagnostics
.iter()
.find(|d| d.rule == "AS-005")
.expect("AS-005 should be reported");
assert!(as_005.has_fixes());
let fix = &as_005.fixes[0];
assert_eq!(fix.replacement, "bad-name");
assert!(fix.safe);
}
#[test]
fn test_as_006_has_safe_fix() {
let content = r#"---
name: bad--name
description: Use when testing
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_006 = diagnostics
.iter()
.find(|d| d.rule == "AS-006")
.expect("AS-006 should be reported");
assert!(as_006.has_fixes());
let fix = &as_006.fixes[0];
assert_eq!(fix.replacement, "bad-name");
assert!(fix.safe);
}
#[test]
fn test_as_014_has_safe_fix() {
let content = r#"---
name: test-skill
description: Use when testing
---
See references\guide.md for details."#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_014 = diagnostics
.iter()
.find(|d| d.rule == "AS-014")
.expect("AS-014 should be reported");
assert!(as_014.has_fixes());
let fix = &as_014.fixes[0];
assert_eq!(fix.replacement, "references/guide.md");
assert!(fix.safe);
}
#[test]
fn test_cc_sk_001_has_unsafe_fix() {
let content = r#"---
name: test-skill
description: Use when testing
model: gpt-4
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_001 = diagnostics
.iter()
.find(|d| d.rule == "CC-SK-001")
.expect("CC-SK-001 should be reported");
assert!(cc_sk_001.has_fixes());
let fix = &cc_sk_001.fixes[0];
assert_eq!(fix.replacement, "sonnet");
assert!(!fix.safe);
}
#[test]
fn test_cc_sk_002_has_unsafe_fix() {
let content = r#"---
name: test-skill
description: Use when testing
context: split
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_002 = diagnostics
.iter()
.find(|d| d.rule == "CC-SK-002")
.expect("CC-SK-002 should be reported");
assert!(cc_sk_002.has_fixes());
let fix = &cc_sk_002.fixes[0];
assert_eq!(fix.replacement, "fork");
assert!(!fix.safe);
}
#[test]
fn test_cc_sk_003_has_insert_fix() {
let content = r#"---
name: test-skill
description: Use when testing
context: fork
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_003 = diagnostics
.iter()
.find(|d| d.rule == "CC-SK-003")
.expect("CC-SK-003 should be reported");
assert!(cc_sk_003.has_fixes());
let fix = &cc_sk_003.fixes[0];
assert!(fix.replacement.contains("agent: general-purpose"));
assert!(!fix.safe);
}
#[test]
fn test_cc_sk_004_has_insert_or_replace_fix() {
let content = r#"---
name: test-skill
description: Use when testing
agent: Explore
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_004 = diagnostics
.iter()
.find(|d| d.rule == "CC-SK-004")
.expect("CC-SK-004 should be reported");
assert!(cc_sk_004.has_fixes());
let fix = &cc_sk_004.fixes[0];
assert!(fix.replacement.contains("context: fork") || fix.replacement == "fork");
assert!(!fix.safe);
}
#[test]
fn test_cc_sk_010_invalid_hook_event() {
let content = r#"---
name: hooks-skill
description: Use when testing hooks
hooks:
InvalidEvent:
- type: command
command: echo hello
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("SKILL.md"), content, &LintConfig::default());
let cc_sk_010: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-010")
.collect();
assert_eq!(cc_sk_010.len(), 1);
assert_eq!(
cc_sk_010[0].level,
crate::diagnostics::DiagnosticLevel::Error
);
assert!(cc_sk_010[0].message.contains("InvalidEvent"));
}
#[test]
fn test_cc_sk_010_valid_hook_event() {
let content = r#"---
name: hooks-skill
description: Use when testing hooks
hooks:
PreToolUse:
- type: command
command: echo pre
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("SKILL.md"), content, &LintConfig::default());
let cc_sk_010: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-010")
.collect();
assert_eq!(cc_sk_010.len(), 0);
}
#[test]
fn test_cc_sk_010_no_hooks_field_ok() {
let content = r#"---
name: simple-skill
description: Use when testing
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("SKILL.md"), content, &LintConfig::default());
let cc_sk_010: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-010")
.collect();
assert_eq!(cc_sk_010.len(), 0);
}
#[test]
fn test_cc_sk_010_hooks_not_mapping() {
let content = r#"---
name: hooks-skill
description: Use when testing hooks
hooks: not-a-mapping
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("SKILL.md"), content, &LintConfig::default());
let cc_sk_010: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-010")
.collect();
assert_eq!(cc_sk_010.len(), 1);
assert!(cc_sk_010[0].message.contains("must be a mapping"));
}
#[test]
fn test_cc_sk_010_fixture_invalid() {
let content =
include_str!("../../../../../tests/fixtures/invalid/skills/invalid-hooks/SKILL.md");
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("SKILL.md"), content, &LintConfig::default());
let cc_sk_010: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-010")
.collect();
assert!(
!cc_sk_010.is_empty(),
"Invalid hooks fixture should trigger CC-SK-010"
);
}
#[test]
fn test_cc_sk_010_fixture_valid() {
let content = include_str!("../../../../../tests/fixtures/valid/skills/with-hooks/SKILL.md");
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("SKILL.md"), content, &LintConfig::default());
let cc_sk_010: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-010")
.collect();
assert_eq!(
cc_sk_010.len(),
0,
"Valid hooks fixture should not trigger CC-SK-010"
);
}
#[test]
fn test_cc_sk_011_unreachable() {
let content = r#"---
name: unreachable
description: Use when testing unreachable
user-invocable: false
disable-model-invocation: true
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_011: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-011")
.collect();
assert_eq!(cc_sk_011.len(), 1);
assert_eq!(
cc_sk_011[0].level,
crate::diagnostics::DiagnosticLevel::Error
);
assert!(cc_sk_011[0].has_fixes(), "CC-SK-011 should have auto-fix");
let fix = &cc_sk_011[0].fixes[0];
assert!(!fix.safe, "CC-SK-011 fix should be unsafe");
assert!(
fix.replacement.is_empty(),
"CC-SK-011 fix should be a deletion"
);
let deleted = &content[fix.start_byte..fix.end_byte];
assert!(
deleted.contains("disable-model-invocation"),
"Fix should target the disable-model-invocation line, got: {:?}",
deleted
);
}
#[test]
fn test_cc_sk_011_user_invocable_true_ok() {
let content = r#"---
name: invocable
description: Use when testing
user-invocable: true
disable-model-invocation: true
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_011: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-011")
.collect();
assert_eq!(cc_sk_011.len(), 0);
}
#[test]
fn test_cc_sk_011_model_invocation_false_ok() {
let content = r#"---
name: invocable
description: Use when testing
user-invocable: false
disable-model-invocation: false
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_011: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-011")
.collect();
assert_eq!(cc_sk_011.len(), 0);
}
#[test]
fn test_cc_sk_011_defaults_ok() {
let content = r#"---
name: default-skill
description: Use when testing
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_011: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-011")
.collect();
assert_eq!(cc_sk_011.len(), 0);
}
#[test]
fn test_cc_sk_011_fixture() {
let content =
include_str!("../../../../../tests/fixtures/invalid/skills/unreachable-skill/SKILL.md");
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("SKILL.md"), content, &LintConfig::default());
let cc_sk_011: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-011")
.collect();
assert_eq!(
cc_sk_011.len(),
1,
"Unreachable skill fixture should trigger CC-SK-011"
);
}
#[test]
fn test_cc_sk_012_hint_without_arguments() {
let content = r#"---
name: hint-skill
description: Use when testing hints
argument-hint: <file-path>
---
Process the given file."#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_012: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-012")
.collect();
assert_eq!(cc_sk_012.len(), 1);
assert_eq!(
cc_sk_012[0].level,
crate::diagnostics::DiagnosticLevel::Warning
);
}
#[test]
fn test_cc_sk_012_hint_with_arguments_ok() {
let content = r#"---
name: hint-skill
description: Use when testing hints
argument-hint: <file-path>
---
Process the file specified in $ARGUMENTS."#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_012: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-012")
.collect();
assert_eq!(cc_sk_012.len(), 0);
}
#[test]
fn test_cc_sk_012_no_hint_ok() {
let content = r#"---
name: no-hint
description: Use when testing
---
Body without $ARGUMENTS."#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_012: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-012")
.collect();
assert_eq!(cc_sk_012.len(), 0);
}
#[test]
fn test_cc_sk_012_fixture_invalid() {
let content =
include_str!("../../../../../tests/fixtures/invalid/skills/argument-hint-no-args/SKILL.md");
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("SKILL.md"), content, &LintConfig::default());
let cc_sk_012: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-012")
.collect();
assert_eq!(
cc_sk_012.len(),
1,
"Argument hint without $ARGUMENTS fixture should trigger CC-SK-012"
);
}
#[test]
fn test_cc_sk_012_fixture_valid() {
let content =
include_str!("../../../../../tests/fixtures/valid/skills/with-argument-hint/SKILL.md");
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("SKILL.md"), content, &LintConfig::default());
let cc_sk_012: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-012")
.collect();
assert_eq!(
cc_sk_012.len(),
0,
"Valid argument hint fixture should not trigger CC-SK-012"
);
}
#[test]
fn test_cc_sk_016_indexed_args_without_hint() {
let content = r#"---
name: indexed-args
description: Use when validating indexed arguments
---
Process path: $ARGUMENTS[0]"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_016: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-016")
.collect();
assert_eq!(cc_sk_016.len(), 1);
assert_eq!(
cc_sk_016[0].level,
crate::diagnostics::DiagnosticLevel::Warning
);
}
#[test]
fn test_cc_sk_016_indexed_args_with_hint_ok() {
let content = r#"---
name: indexed-args
description: Use when validating indexed arguments
argument-hint: <path>
---
Process path: $ARGUMENTS[0]"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_016: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-016")
.collect();
assert_eq!(cc_sk_016.len(), 0);
}
#[test]
fn test_cc_sk_017_unknown_frontmatter_field() {
let content = r#"---
name: test-skill
description: Use when validating unknown frontmatter fields
desription: typo field
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_017: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-017")
.collect();
assert_eq!(cc_sk_017.len(), 1);
assert!(cc_sk_017[0].message.contains("desription"));
}
#[test]
fn test_cc_sk_017_known_frontmatter_field_ok() {
let content = r#"---
name: test-skill
description: Use when validating known frontmatter fields
hooks:
PreToolUse:
- type: command
command: echo pre
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_017: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-017")
.collect();
assert_eq!(cc_sk_017.len(), 0);
}
#[test]
fn test_cc_sk_013_fork_without_instructions() {
let content = r#"---
name: ref-skill
description: Use when looking up docs
context: fork
agent: general-purpose
---
This is a reference document about the API.
It describes the system architecture.
The data models are documented here."#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_013: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-013")
.collect();
assert_eq!(cc_sk_013.len(), 1);
assert_eq!(
cc_sk_013[0].level,
crate::diagnostics::DiagnosticLevel::Warning
);
}
#[test]
fn test_cc_sk_013_fork_with_instructions_ok() {
let content = r#"---
name: build-skill
description: Use when building the project
context: fork
agent: general-purpose
---
Run the build command and check for errors.
Create a report of the results."#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_013: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-013")
.collect();
assert_eq!(cc_sk_013.len(), 0);
}
#[test]
fn test_cc_sk_013_no_fork_ok() {
let content = r#"---
name: ref-skill
description: Use when looking up docs
---
This is just reference content without imperative verbs."#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_013: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-013")
.collect();
assert_eq!(cc_sk_013.len(), 0);
}
#[test]
fn test_cc_sk_013_fixture_invalid() {
let content =
include_str!("../../../../../tests/fixtures/invalid/skills/fork-no-instructions/SKILL.md");
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("SKILL.md"), content, &LintConfig::default());
let cc_sk_013: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-013")
.collect();
assert_eq!(
cc_sk_013.len(),
1,
"Fork without instructions fixture should trigger CC-SK-013"
);
}
#[test]
fn test_cc_sk_013_fixture_valid() {
let content =
include_str!("../../../../../tests/fixtures/valid/skills/fork-with-instructions/SKILL.md");
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("SKILL.md"), content, &LintConfig::default());
let cc_sk_013: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-013")
.collect();
assert_eq!(
cc_sk_013.len(),
0,
"Fork with instructions fixture should not trigger CC-SK-013"
);
}
#[test]
fn test_cc_sk_014_string_true() {
let content = r#"---
name: test-skill
description: Use when testing
disable-model-invocation: "true"
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_014: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-014")
.collect();
assert_eq!(cc_sk_014.len(), 1);
assert_eq!(
cc_sk_014[0].level,
crate::diagnostics::DiagnosticLevel::Error
);
assert!(cc_sk_014[0].message.contains("true"));
}
#[test]
fn test_cc_sk_014_string_false() {
let content = r#"---
name: test-skill
description: Use when testing
disable-model-invocation: "false"
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_014: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-014")
.collect();
assert_eq!(cc_sk_014.len(), 1);
}
#[test]
fn test_cc_sk_014_boolean_true_ok() {
let content = r#"---
name: test-skill
description: Use when testing
disable-model-invocation: true
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_014: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-014")
.collect();
assert_eq!(cc_sk_014.len(), 0);
}
#[test]
fn test_cc_sk_014_has_safe_fix() {
let content = r#"---
name: test-skill
description: Use when testing
disable-model-invocation: "true"
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_014: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-014")
.collect();
assert_eq!(cc_sk_014.len(), 1);
assert!(cc_sk_014[0].has_fixes());
let fix = &cc_sk_014[0].fixes[0];
assert_eq!(fix.replacement, "true");
assert!(fix.safe);
}
#[test]
fn test_cc_sk_014_fix_applies_correctly() {
let content = r#"---
name: test-skill
description: Use when testing
disable-model-invocation: "true"
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_014: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-014")
.collect();
assert_eq!(cc_sk_014.len(), 1);
assert!(cc_sk_014[0].has_fixes());
let fix = &cc_sk_014[0].fixes[0];
let mut fixed = content.to_string();
fixed.replace_range(fix.start_byte..fix.end_byte, &fix.replacement);
assert!(fixed.contains("disable-model-invocation: true"));
assert!(!fixed.contains("\"true\""));
}
#[test]
fn test_cc_sk_014_single_quoted() {
let content = "---\nname: test-skill\ndescription: Use when testing\ndisable-model-invocation: 'true'\n---\nBody";
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_014: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-014")
.collect();
assert_eq!(cc_sk_014.len(), 1);
}
#[test]
fn test_cc_sk_015_string_true() {
let content = r#"---
name: test-skill
description: Use when testing
user-invocable: "true"
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_015: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-015")
.collect();
assert_eq!(cc_sk_015.len(), 1);
assert_eq!(
cc_sk_015[0].level,
crate::diagnostics::DiagnosticLevel::Error
);
}
#[test]
fn test_cc_sk_015_string_false() {
let content = r#"---
name: test-skill
description: Use when testing
user-invocable: "false"
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_015: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-015")
.collect();
assert_eq!(cc_sk_015.len(), 1);
}
#[test]
fn test_cc_sk_015_boolean_false_ok() {
let content = r#"---
name: test-skill
description: Use when testing
user-invocable: false
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_015: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-015")
.collect();
assert_eq!(cc_sk_015.len(), 0);
}
#[test]
fn test_cc_sk_015_has_safe_fix() {
let content = r#"---
name: test-skill
description: Use when testing
user-invocable: "false"
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_015: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-015")
.collect();
assert_eq!(cc_sk_015.len(), 1);
assert!(cc_sk_015[0].has_fixes());
let fix = &cc_sk_015[0].fixes[0];
assert_eq!(fix.replacement, "false");
assert!(fix.safe);
}
#[test]
fn test_cc_sk_015_fix_applies_correctly() {
let content = r#"---
name: test-skill
description: Use when testing
user-invocable: "false"
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_015: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-015")
.collect();
assert_eq!(cc_sk_015.len(), 1);
assert!(cc_sk_015[0].has_fixes());
let fix = &cc_sk_015[0].fixes[0];
let mut fixed = content.to_string();
fixed.replace_range(fix.start_byte..fix.end_byte, &fix.replacement);
assert!(fixed.contains("user-invocable: false"));
assert!(!fixed.contains("\"false\""));
}
#[test]
fn test_cc_sk_014_fixture() {
let content = include_str!(
"../../../../../tests/fixtures/invalid/skills/string-boolean-disable/SKILL.md"
);
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("SKILL.md"), content, &LintConfig::default());
let cc_sk_014: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-014")
.collect();
assert_eq!(
cc_sk_014.len(),
1,
"String boolean disable fixture should trigger CC-SK-014"
);
}
#[test]
fn test_cc_sk_015_fixture() {
let content = include_str!(
"../../../../../tests/fixtures/invalid/skills/string-boolean-invocable/SKILL.md"
);
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("SKILL.md"), content, &LintConfig::default());
let cc_sk_015: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-015")
.collect();
assert_eq!(
cc_sk_015.len(),
1,
"String boolean invocable fixture should trigger CC-SK-015"
);
}
#[test]
fn test_cc_sk_014_with_inline_comment() {
let content = "---\nname: test-skill\ndescription: Use when testing\ndisable-model-invocation: \"true\" # some comment\n---\nBody";
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_014: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-014")
.collect();
assert_eq!(
cc_sk_014.len(),
1,
"Should detect quoted boolean even with trailing inline comment"
);
}
#[test]
fn test_cc_sk_015_with_inline_comment() {
let content = "---\nname: test-skill\ndescription: Use when testing\nuser-invocable: \"false\" # override default\n---\nBody";
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_015: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-015")
.collect();
assert_eq!(
cc_sk_015.len(),
1,
"Should detect quoted boolean even with trailing inline comment"
);
}
#[test]
fn test_cc_sk_014_does_not_produce_as_016() {
let content = r#"---
name: test-skill
description: Use when testing
disable-model-invocation: "true"
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_016: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-016").collect();
let cc_sk_014: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-014")
.collect();
assert_eq!(cc_sk_014.len(), 1, "CC-SK-014 should fire for quoted bool");
let _ = as_016;
}
#[test]
fn test_cc_sk_013_empty_body_fork() {
let content = "---\nname: test-skill\ndescription: Use when testing\ncontext: fork\n---\n";
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_013: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-013")
.collect();
assert_eq!(
cc_sk_013.len(),
1,
"Empty body with fork context should trigger CC-SK-013"
);
}
#[test]
fn test_cc_sk_013_whitespace_only_body_fork() {
let content =
"---\nname: test-skill\ndescription: Use when testing\ncontext: fork\n---\n \n \n";
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_013: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-013")
.collect();
assert_eq!(
cc_sk_013.len(),
1,
"Whitespace-only body with fork context should trigger CC-SK-013"
);
}
#[test]
fn test_cc_sk_005_autofix_invalid_agent() {
let content = "---\nname: my-skill\ndescription: Use when testing\ncontext: fork\nagent: INVALID_AGENT\n---\nRun the tests.";
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_005: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-005")
.collect();
assert_eq!(cc_sk_005.len(), 1);
assert!(
cc_sk_005[0].has_fixes(),
"CC-SK-005 should have auto-fix for invalid agent"
);
let fix = &cc_sk_005[0].fixes[0];
assert!(!fix.safe, "CC-SK-005 fix should be unsafe");
assert_eq!(
fix.replacement, "general-purpose",
"Fix should replace with 'general-purpose'"
);
let target = &content[fix.start_byte..fix.end_byte];
assert_eq!(target, "INVALID_AGENT");
}
#[test]
fn test_cc_sk_005_no_fix_for_valid_agent() {
let content = "---\nname: my-skill\ndescription: Use when testing\ncontext: fork\nagent: general-purpose\n---\nRun the tests.";
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_005: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-005")
.collect();
assert!(cc_sk_005.is_empty());
}
#[test]
fn test_as_016_has_suggestion() {
let content = "---\n invalid: [yaml\n---\ncontent";
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let parse_errors: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-016").collect();
assert_eq!(parse_errors.len(), 1);
assert!(
parse_errors[0].suggestion.is_some(),
"AS-016 should have a suggestion"
);
assert!(
parse_errors[0]
.suggestion
.as_ref()
.unwrap()
.contains("YAML frontmatter syntax"),
"AS-016 suggestion should mention YAML frontmatter syntax"
);
}
#[test]
fn test_cc_sk_006_has_fix() {
let content = "---\nname: deploy-app\ndescription: Use when deploying the app\n---\nDeploy the application.";
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_006: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-006")
.collect();
assert_eq!(cc_sk_006.len(), 1);
assert!(cc_sk_006[0].has_fixes(), "CC-SK-006 should have auto-fix");
let fix = &cc_sk_006[0].fixes[0];
assert!(
!fix.safe,
"CC-SK-006 fix should be unsafe (changes runtime behavior)"
);
assert!(
fix.replacement.contains("disable-model-invocation: true"),
"Fix should insert disable-model-invocation: true"
);
}
#[test]
fn test_cc_sk_012_has_fix() {
let content = "---\nname: greet-user\ndescription: Use when greeting users\nargument-hint: Name of person to greet\n---\nGreet the user warmly.";
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_012: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-012")
.collect();
assert_eq!(cc_sk_012.len(), 1);
assert!(cc_sk_012[0].has_fixes(), "CC-SK-012 should have auto-fix");
let fix = &cc_sk_012[0].fixes[0];
assert!(
!fix.safe,
"CC-SK-012 fix should be unsafe (appends to body)"
);
assert!(
fix.replacement.contains("$ARGUMENTS"),
"Fix should append $ARGUMENTS"
);
}
#[test]
fn test_as_001_has_fix() {
let content = "Some skill content without frontmatter.";
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_001: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-001").collect();
assert_eq!(as_001.len(), 1);
assert!(as_001[0].has_fixes(), "AS-001 should have auto-fix");
let fix = &as_001[0].fixes[0];
assert!(!fix.safe, "AS-001 fix should be unsafe");
assert!(
fix.replacement.contains("---"),
"Fix should insert frontmatter block"
);
}
#[test]
fn test_as_002_has_fix() {
let content = "---\ndescription: Use when testing\n---\nBody";
let validator = SkillValidator;
let diagnostics =
validator.validate(Path::new("test-skill.md"), content, &LintConfig::default());
let as_002: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-002").collect();
assert_eq!(as_002.len(), 1);
assert!(as_002[0].has_fixes(), "AS-002 should have auto-fix");
let fix = &as_002[0].fixes[0];
assert!(!fix.safe, "AS-002 fix should be unsafe");
assert!(
fix.replacement.contains("name:"),
"Fix should insert name field"
);
}
#[test]
fn test_as_003_has_fix() {
let content = "---\nname: test-skill\n---\nBody";
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_003: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-003").collect();
assert_eq!(as_003.len(), 1);
assert!(as_003[0].has_fixes(), "AS-003 should have auto-fix");
let fix = &as_003[0].fixes[0];
assert!(!fix.safe, "AS-003 fix should be unsafe");
assert!(
fix.replacement.contains("description:"),
"Fix should insert description placeholder"
);
}
#[test]
fn test_as_009_has_fix() {
let content = "---\nname: test-skill\ndescription: <b>Use when testing</b>\n---\nBody";
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let as_009: Vec<_> = diagnostics.iter().filter(|d| d.rule == "AS-009").collect();
assert_eq!(as_009.len(), 1);
assert!(
as_009[0].has_fixes(),
"AS-009 should have auto-fix to strip XML tags"
);
let fix = &as_009[0].fixes[0];
assert!(!fix.safe, "AS-009 fix should be unsafe");
assert!(
!fix.replacement.contains('<'),
"Fix should strip XML tags from description"
);
}
#[test]
fn test_cc_sk_018_invalid_effort() {
let content = r#"---
name: test-skill
description: Use when testing effort validation
effort: turbo
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_018: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-018")
.collect();
assert_eq!(cc_sk_018.len(), 1);
assert_eq!(
cc_sk_018[0].level,
crate::diagnostics::DiagnosticLevel::Warning
);
assert!(cc_sk_018[0].message.contains("turbo"));
}
#[test]
fn test_cc_sk_018_valid_effort_values() {
for effort in VALID_EFFORT_LEVELS {
let content = format!(
"---\nname: test-skill\ndescription: Use when testing effort\neffort: {}\n---\nBody",
effort
);
let validator = SkillValidator;
let diagnostics =
validator.validate(Path::new("test.md"), &content, &LintConfig::default());
let cc_sk_018: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-018")
.collect();
assert_eq!(cc_sk_018.len(), 0, "Effort '{}' should be valid", effort);
}
}
#[test]
fn test_cc_sk_018_no_effort_ok() {
let content = r#"---
name: test-skill
description: Use when testing
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_018: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-018")
.collect();
assert_eq!(cc_sk_018.len(), 0);
}
#[test]
fn test_cc_sk_019_empty_paths() {
let content =
"---\nname: test-skill\ndescription: Use when testing paths\npaths: \"\"\n---\nBody";
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_019: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-019")
.collect();
assert_eq!(cc_sk_019.len(), 1);
assert_eq!(
cc_sk_019[0].level,
crate::diagnostics::DiagnosticLevel::Info
);
}
#[test]
fn test_cc_sk_019_whitespace_only_paths() {
let content =
"---\nname: test-skill\ndescription: Use when testing paths\npaths: \" \"\n---\nBody";
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_019: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-019")
.collect();
assert_eq!(cc_sk_019.len(), 1);
assert_eq!(
cc_sk_019[0].level,
crate::diagnostics::DiagnosticLevel::Info
);
}
#[test]
fn test_cc_sk_019_valid_paths() {
let content = r#"---
name: test-skill
description: Use when testing paths
paths: "src/**/*.ts"
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_019: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-019")
.collect();
assert_eq!(cc_sk_019.len(), 0);
}
#[test]
fn test_cc_sk_020_invalid_shell() {
let content = r#"---
name: test-skill
description: Use when testing shell validation
shell: zsh
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_020: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-020")
.collect();
assert_eq!(cc_sk_020.len(), 1);
assert_eq!(
cc_sk_020[0].level,
crate::diagnostics::DiagnosticLevel::Warning
);
assert!(cc_sk_020[0].message.contains("zsh"));
}
#[test]
fn test_cc_sk_020_valid_shell_values() {
for shell in &["bash", "powershell"] {
let content = format!(
"---\nname: test-skill\ndescription: Use when testing shell\nshell: {}\n---\nBody",
shell
);
let validator = SkillValidator;
let diagnostics =
validator.validate(Path::new("test.md"), &content, &LintConfig::default());
let cc_sk_020: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-020")
.collect();
assert_eq!(cc_sk_020.len(), 0, "Shell '{}' should be valid", shell);
}
}
#[test]
fn test_cc_sk_020_no_shell_ok() {
let content = r#"---
name: test-skill
description: Use when testing
---
Body"#;
let validator = SkillValidator;
let diagnostics = validator.validate(Path::new("test.md"), content, &LintConfig::default());
let cc_sk_020: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-SK-020")
.collect();
assert_eq!(cc_sk_020.len(), 0);
}