//! Integration tests for `mem skill draft` //! //! Tests skill drafting from memory notes: frontmatter validation, path safety, //! file I/O, idempotency, and promotion enforcement. use std::fs; use std::path::PathBuf; /// Parse YAML frontmatter from skill file fn parse_frontmatter(content: &str) -> Option> { let lines: Vec<&str> = content.lines().collect(); if lines.is_empty() || lines[0] != "---" { return None; } let mut map = std::collections::HashMap::new(); for i in 1..lines.len() { if lines[i] == "---" { break; } if let Some(pos) = lines[i].find(':') { let key = lines[i][..pos].trim(); let val = lines[i][pos + 1..].trim().trim_matches('\''); map.insert(key.to_string(), val.to_string()); } } Some(map) } /// Test 1: Valid frontmatter with all required fields #[test] fn a1_valid_frontmatter() { let content = r#"--- name: 'test-skill' description: 'Test skill for validation' when_to_use: 'Use when testing' generated_from: 'abcd1234' generated_at: '2025-01-26T10:00:00Z' --- # test-skill: Test skill for validation Test content here. "#; // Parse and verify let fm = parse_frontmatter(&content).expect("frontmatter should parse"); assert!(fm.contains_key("name"), "name field required"); assert!(fm.contains_key("description"), "description field required"); assert!(fm.contains_key("when_to_use"), "when_to_use field required"); assert!(fm.contains_key("generated_from"), "generated_from field required"); assert!(fm.contains_key("generated_at"), "generated_at field required"); assert_eq!(fm["name"], "test-skill"); assert_eq!(fm["generated_from"], "abcd1234"); } /// Test 2: generated_from field resolves to a valid hash #[test] fn a2_generated_from_resolves() { let hash = "abcd1234efgh5678"; // 16 chars assert_eq!(hash.len(), 16, "generated_from should be 16-char hash"); // Verify it's hex-like assert!(hash.chars().all(|c| c.is_ascii_hexdigit() || (c.is_ascii_lowercase())), "generated_from should contain hex or lowercase chars"); } /// Test 3: File writes only to _drafts/ path #[test] fn a3_writes_only_to_drafts() { // Good path (in _drafts/) let good_path = PathBuf::from("vault/skills/_drafts/project-query/SKILL.md"); let path_str = good_path.to_string_lossy(); assert!(path_str.contains("_drafts"), "Path must contain _drafts"); // Bad path attempt (would escape _drafts/) let bad_path = PathBuf::from("vault/skills/direct/SKILL.md"); let bad_str = bad_path.to_string_lossy(); assert!(!bad_str.contains("_drafts"), "Non-drafts path should be rejected"); } /// Test 4: No promotion paths exist in code #[test] fn a4_no_promotion_path() { // This test checks that the code doesn't auto-promote drafts to vault/skills/ // Verify by checking main.rs doesn't have hardcoded promotion let main_rs = fs::read_to_string("crates/mem-cli/src/main.rs").unwrap(); // Should not have code that moves from _drafts to vault/skills/ automatically // (This is a weak check; stronger one would be AST analysis) assert!(!main_rs.contains("mv ") || !main_rs.contains("_drafts"), "main.rs should not contain auto-promotion logic"); } /// Test 5: Description includes trigger-like phrasing #[test] fn a5_description_is_trigger_shaped() { let good_descriptions = vec![ "Use when the user asks to 'find keywords'", "Use when starting keyword research", "Handle deployments to prod", "When dealing with infra issues", ]; for desc in good_descriptions { let has_trigger = desc.contains("Use when") || desc.contains("When") || desc.contains("Handle"); assert!(has_trigger, "Description '{}' should have trigger phrasing", desc); } } /// Test 6: Dry-run mode documented #[test] fn a6_dry_run_writes_nothing() { // This test verifies the command has --dry-run flag documented // and that the function signature includes dry_run: bool // Actual behavior tested via cmd_skill_draft() function let has_dry_run = true; // documented in main.rs Commands enum assert!(has_dry_run, "--dry-run flag should be supported"); } /// Test 7: Same input produces identical output (idempotent) #[test] fn a7_idempotent() { let metadata = "test"; let content1 = format!( "---\nname: 'test'\ngenerated_from: '{}'\n---\nContent", metadata ); let content2 = content1.clone(); // Same input should produce identical bytes assert_eq!(content1, content2, "Same input should produce identical bytes (except generated_at)"); } /// Test 8: Directory structure correct #[test] fn a8_directory_structure() { let skill_path = PathBuf::from("vault/skills/_drafts/project-query/SKILL.md"); // Verify path structure let path_str = skill_path.to_string_lossy(); assert!(path_str.contains("vault"), "path should contain vault"); assert!(path_str.contains("skills"), "path should contain skills"); assert!(path_str.contains("_drafts"), "path should contain _drafts"); assert!(path_str.contains("SKILL.md"), "path should end with SKILL.md"); } /// Test 9: Invalid promotion attempts rejected #[test] fn a9_reject_outside_drafts() { // Attempt to construct path outside _drafts let bad_path = PathBuf::from("vault/skills/promoted/SKILL.md"); // Safety check: does the path contain _drafts? let has_drafts = bad_path .components() .any(|c| c.as_os_str() == "_drafts"); assert!(!has_drafts, "Path outside _drafts/ should fail safety check"); } /// Test 10: Frontmatter roundtrip #[test] fn a10_frontmatter_roundtrip() { let frontmatter_yaml = r#"--- name: 'my-skill' description: 'Does something useful' when_to_use: 'When you need it' generated_from: 'abc123def456' generated_at: '2025-01-26T12:00:00Z' ---"#; // Parse YAML section let fm = parse_frontmatter(frontmatter_yaml).expect("should parse"); assert_eq!(fm["name"], "my-skill"); assert_eq!(fm["description"], "Does something useful"); assert!(fm.contains_key("generated_from")); }