//! Public-API tests for skill registration and enumeration. //! //! These exercise what a host can actually reach: registering roots through //! `Runtime`, listing what loaded, and naming the error type in its own //! signatures. use std::{ fs, path::{Path, PathBuf}, sync::atomic::{AtomicU64, Ordering}, time::{SystemTime, UNIX_EPOCH}, }; use mentra::{BuiltinProvider, Runtime, SkillInfo, SkillLoadError}; static NEXT_ID: AtomicU64 = AtomicU64::new(0); fn temp_dir(label: &str) -> PathBuf { let unique = NEXT_ID.fetch_add(1, Ordering::Relaxed); let stamp = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("system time") .as_nanos(); let path = std::env::temp_dir().join(format!("mentra-skills-api-{label}-{stamp}-{unique}")); fs::create_dir_all(&path).expect("create temp dir"); path } fn write_skill(root: &Path, dir: &str, name: &str, description: &str, body: &str) { let skill_dir = root.join(dir); fs::create_dir_all(&skill_dir).expect("create skill dir"); fs::write( skill_dir.join("SKILL.md"), format!("---\nname: {name}\ndescription: {description}\n---\n{body}\n"), ) .expect("write skill"); } fn runtime() -> Runtime { Runtime::builder() .with_provider(BuiltinProvider::OpenAI, "test-key") .build() .expect("runtime builds") } /// The error type must be nameable by a caller — this signature is the test. fn register(runtime: &Runtime, path: &Path) -> Result<(), SkillLoadError> { runtime.register_skills_dir(path) } #[test] fn a_host_can_name_the_error_type() { let runtime = runtime(); let missing = temp_dir("nameable").join("does-not-exist"); let error = register(&runtime, &missing).expect_err("an unreadable root is an error"); // And match on it, which is the point of it being an enum. assert!(matches!(error, SkillLoadError::ReadDir { .. })); } #[test] fn registering_a_second_root_keeps_the_first() { let workspace = temp_dir("layer-workspace"); write_skill(&workspace, "review", "review", "project review", "PROJECT"); let global = temp_dir("layer-global"); write_skill(&global, "review", "review", "personal review", "PERSONAL"); write_skill(&global, "deploy", "deploy", "personal deploy", "DEPLOY"); let runtime = runtime(); runtime .register_skills_dirs([workspace.as_path(), global.as_path()]) .expect("both roots register"); let skills = runtime.skills(); let names: Vec<&str> = skills.iter().map(|skill| skill.name.as_str()).collect(); assert_eq!(names, vec!["deploy", "review"]); let review = skills .iter() .find(|skill| skill.name == "review") .expect("review present"); assert_eq!( review.description, "project review", "the earlier root must win the collision" ); assert!(review.path.starts_with(&workspace)); } #[test] fn enumeration_reports_name_description_and_source() { let root = temp_dir("enumerate"); write_skill(&root, "haiku", "haiku", "writes haiku", "BODY"); let runtime = runtime(); runtime.register_skills_dir(&root).expect("registers"); let skills = runtime.skills(); assert_eq!(skills.len(), 1); let SkillInfo { name, description, path, } = &skills[0]; assert_eq!(name, "haiku"); assert_eq!(description, "writes haiku"); assert_eq!(path, &root.join("haiku").join("SKILL.md")); } #[test] fn a_runtime_without_skills_lists_none() { assert!(runtime().skills().is_empty()); } #[test] fn a_duplicate_name_inside_one_root_is_still_an_error() { let root = temp_dir("duplicate"); write_skill(&root, "first", "shared", "one", "A"); write_skill(&root, "second", "shared", "two", "B"); let error = runtime() .register_skills_dir(&root) .expect_err("a repeated name in one root is a mistake"); assert!(matches!(error, SkillLoadError::DuplicateSkillName { .. })); } #[test] fn roots_before_a_failing_one_stay_registered() { let good = temp_dir("partial-good"); write_skill(&good, "keep", "keep", "kept", "BODY"); let missing = temp_dir("partial-missing").join("absent"); let runtime = runtime(); let error = runtime .register_skills_dirs([good.as_path(), missing.as_path()]) .expect_err("the second root fails"); assert!(matches!(error, SkillLoadError::ReadDir { .. })); assert_eq!( runtime.skills().len(), 1, "the root that loaded before the failure stays registered" ); }