use std::{path::PathBuf, sync::Arc};
use crate::{error::RunError, runtime::Runtime};
#[derive(Debug)]
pub(crate) struct SkillRoots {
runtime: Arc<Runtime>,
dirs: Vec<PathBuf>,
}
impl SkillRoots {
pub(crate) fn register(runtime: Arc<Runtime>, dirs: Vec<PathBuf>) -> Result<Self, RunError> {
runtime.register_skill_roots(&dirs)?;
Ok(Self { runtime, dirs })
}
pub(crate) fn dirs(&self) -> &[PathBuf] {
&self.dirs
}
}
impl Drop for SkillRoots {
fn drop(&mut self) {
self.runtime.release_skill_roots(&self.dirs);
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::*;
fn write(path: &Path, body: &str) {
std::fs::create_dir_all(path.parent().expect("a parent")).expect("create dir");
std::fs::write(path, body).expect("write file");
}
fn skill_root(dir: &Path, name: &str) -> PathBuf {
write(
&dir.join(name).join("SKILL.md"),
&format!("---\nname: {name}\ndescription: a skill\n---\nSteps."),
);
dir.to_path_buf()
}
fn runtime() -> Arc<Runtime> {
Arc::new(
Runtime::builder()
.with_base_url("http://127.0.0.1:1/v1")
.with_api_key("test-key")
.with_ephemeral_history()
.build()
.expect("the runtime builds"),
)
}
fn names(runtime: &Runtime) -> Vec<String> {
let mut names: Vec<String> = runtime
.mentra_runtime()
.skills()
.into_iter()
.map(|skill| skill.name)
.collect();
names.sort();
names
}
#[test]
fn dropping_the_guard_takes_the_roots_off_the_runtime() {
let dir = tempfile::tempdir().expect("tempdir");
let root = skill_root(dir.path(), "release");
let runtime = runtime();
let held =
SkillRoots::register(Arc::clone(&runtime), vec![root.clone()]).expect("roots register");
assert_eq!(held.dirs(), [root]);
assert_eq!(names(&runtime), ["release"]);
drop(held);
assert!(names(&runtime).is_empty());
}
#[test]
fn a_root_two_guards_hold_goes_with_the_second_of_them() {
let dir = tempfile::tempdir().expect("tempdir");
let root = skill_root(dir.path(), "personal");
let runtime = runtime();
let first = SkillRoots::register(Arc::clone(&runtime), vec![root.clone()])
.expect("first registers");
let second =
SkillRoots::register(Arc::clone(&runtime), vec![root]).expect("second registers");
drop(first);
assert_eq!(names(&runtime), ["personal"], "the second holder remains");
drop(second);
assert!(names(&runtime).is_empty());
}
#[test]
fn a_refused_registration_holds_nothing() {
let dir = tempfile::tempdir().expect("tempdir");
let good = skill_root(&dir.path().join("good"), "release");
let bad = dir.path().join("bad");
write(
&bad.join("broken").join("SKILL.md"),
"---\nname: [not a string\n---\nbody",
);
let runtime = runtime();
let held = SkillRoots::register(Arc::clone(&runtime), vec![good.clone()])
.expect("the good root registers");
let error = SkillRoots::register(Arc::clone(&runtime), vec![good, bad])
.expect_err("a root mentra cannot load refuses the batch");
assert!(error.to_string().contains("frontmatter"), "{error}");
assert_eq!(
names(&runtime),
["release"],
"the refused batch neither registered nor released anything"
);
drop(held);
assert!(names(&runtime).is_empty());
}
#[test]
fn two_spellings_of_one_root_are_one_hold() {
let dir = tempfile::tempdir().expect("tempdir");
let root = skill_root(&dir.path().join("skills"), "release");
let detour = dir.path().join("skills").join("..").join("skills");
let runtime = runtime();
let first =
SkillRoots::register(Arc::clone(&runtime), vec![root]).expect("first registers");
let second =
SkillRoots::register(Arc::clone(&runtime), vec![detour]).expect("second registers");
drop(first);
assert_eq!(
names(&runtime),
["release"],
"the detour spelling is the same root and still holds it"
);
drop(second);
assert!(names(&runtime).is_empty());
}
}