use super::*;
use tempfile::TempDir;
const BASH_SCHEMA: &str =
r#"{"type":"object","properties":{"command":{"type":"string"}},"required":["command"]}"#;
const BASH_SKILL: &str =
"---\nname: bash\ndescription: Run a shell command.\n---\n\n# bash\n\nbody text\n";
struct Pool {
dir: TempDir,
}
impl Pool {
fn new() -> Self {
Self {
dir: TempDir::new().unwrap(),
}
}
fn root(&self) -> &Path {
self.dir.path()
}
fn tool(&self, name: &str, schema: &str) -> &Self {
let d = self.root().join(TOOLS_SUBDIR);
fs::create_dir_all(&d).unwrap();
fs::write(d.join(format!("{name}.json")), schema).unwrap();
self
}
fn skill(&self, name: &str, skill_md: &str) -> &Self {
let d = self.root().join(SKILLS_SUBDIR).join(name);
fs::create_dir_all(&d).unwrap();
fs::write(d.join(SKILL_MANIFEST), skill_md).unwrap();
self
}
}
fn worktree() -> TempDir {
TempDir::new().unwrap()
}
#[test]
fn copies_tool_schema_verbatim() {
let pool = Pool::new();
pool.tool("bash", BASH_SCHEMA);
let wt = worktree();
snapshot(pool.root(), wt.path()).unwrap();
let out = wt.path().join("descriptions/tools/bash.json");
assert_eq!(fs::read_to_string(&out).unwrap(), BASH_SCHEMA);
}
#[test]
fn writes_skill_frontmatter_body_only() {
let pool = Pool::new();
pool.skill("bash", BASH_SKILL);
let wt = worktree();
snapshot(pool.root(), wt.path()).unwrap();
let out = wt.path().join("descriptions/skills/bash.md");
let body = fs::read_to_string(&out).unwrap();
assert_eq!(body, "name: bash\ndescription: Run a shell command.\n");
let fm = crate::skill::parse(&body).unwrap();
assert_eq!(fm.description, "Run a shell command.");
}
#[test]
fn absent_pools_yield_empty_descriptions_tree() {
let wt = worktree();
snapshot(Path::new("/no/such/data/root"), wt.path()).unwrap();
assert!(!wt.path().join("descriptions").exists());
}
#[test]
fn empty_pool_dirs_create_no_descriptions_subdirs() {
let pool = Pool::new();
fs::create_dir_all(pool.root().join(TOOLS_SUBDIR)).unwrap();
fs::create_dir_all(pool.root().join(SKILLS_SUBDIR)).unwrap();
let wt = worktree();
snapshot(pool.root(), wt.path()).unwrap();
assert!(!wt.path().join("descriptions/tools").exists());
assert!(!wt.path().join("descriptions/skills").exists());
}
#[test]
fn non_json_pool_files_and_non_dir_skill_entries_are_skipped() {
let pool = Pool::new();
pool.tool("bash", BASH_SCHEMA);
fs::write(pool.root().join(TOOLS_SUBDIR).join("README.md"), b"x").unwrap();
fs::create_dir_all(pool.root().join(TOOLS_SUBDIR).join("notafile.json")).unwrap();
fs::create_dir_all(pool.root().join(SKILLS_SUBDIR)).unwrap();
fs::write(pool.root().join(SKILLS_SUBDIR).join("loose.txt"), b"x").unwrap();
let wt = worktree();
snapshot(pool.root(), wt.path()).unwrap();
assert!(wt.path().join("descriptions/tools/bash.json").is_file());
assert!(!wt.path().join("descriptions/tools/README.md").exists());
assert!(!wt.path().join("descriptions/tools/notafile.json").exists());
assert!(!wt.path().join("descriptions/skills").exists());
}
#[test]
fn skill_dir_without_manifest_is_not_an_available_skill() {
let pool = Pool::new();
fs::create_dir_all(pool.root().join(SKILLS_SUBDIR).join("empty-skill")).unwrap();
let wt = worktree();
snapshot(pool.root(), wt.path()).unwrap();
assert!(!wt.path().join("descriptions/skills").exists());
}
#[test]
fn skill_manifest_without_frontmatter_is_a_loud_error() {
let pool = Pool::new();
pool.skill("broken", "no frontmatter here\n");
let wt = worktree();
let err = snapshot(pool.root(), wt.path()).unwrap_err();
match err {
Error::NoFrontmatter { name } => assert_eq!(name, "broken"),
other => panic!("expected NoFrontmatter, got {other:?}"),
}
assert!(
snapshot(pool.root(), wt.path())
.unwrap_err()
.to_string()
.contains("SKILL.md"),
);
}
#[test]
fn snapshot_is_deterministic_across_many_artifacts() {
let pool = Pool::new();
pool.tool("bash", BASH_SCHEMA)
.tool("read_file", r#"{"type":"object"}"#);
pool.skill("bash", BASH_SKILL).skill(
"read_file",
"---\nname: read_file\ndescription: Read a file.\n---\n",
);
let wt = worktree();
snapshot(pool.root(), wt.path()).unwrap();
for name in ["bash", "read_file"] {
assert!(
wt.path()
.join(format!("descriptions/tools/{name}.json"))
.is_file()
);
assert!(
wt.path()
.join(format!("descriptions/skills/{name}.md"))
.is_file()
);
}
}
#[test]
fn tool_copy_failure_surfaces_io_error() {
let pool = Pool::new();
pool.tool("bash", BASH_SCHEMA);
let wt = worktree();
fs::create_dir_all(wt.path().join("descriptions/tools/bash.json")).unwrap();
let err = snapshot(pool.root(), wt.path()).unwrap_err();
assert!(matches!(err, Error::Io { .. }), "got {err:?}");
}
#[test]
fn tool_dest_dir_creation_failure_surfaces_io_error() {
let pool = Pool::new();
pool.tool("bash", BASH_SCHEMA);
let wt = worktree();
fs::write(wt.path().join("descriptions"), b"blocker").unwrap();
let err = snapshot(pool.root(), wt.path()).unwrap_err();
assert!(matches!(err, Error::Io { .. }), "got {err:?}");
}
#[test]
fn skill_manifest_read_failure_surfaces_io_error() {
let pool = Pool::new();
let d = pool.root().join(SKILLS_SUBDIR).join("bad");
fs::create_dir_all(d.join(SKILL_MANIFEST)).unwrap();
let wt = worktree();
let err = snapshot(pool.root(), wt.path()).unwrap_err();
assert!(matches!(err, Error::Io { .. }), "got {err:?}");
}
#[test]
fn skill_dest_dir_creation_failure_surfaces_io_error() {
let pool = Pool::new();
pool.skill("bash", BASH_SKILL);
let wt = worktree();
fs::write(wt.path().join("descriptions"), b"blocker").unwrap();
let err = snapshot(pool.root(), wt.path()).unwrap_err();
assert!(matches!(err, Error::Io { .. }), "got {err:?}");
}
#[test]
fn skill_write_failure_surfaces_io_error() {
let pool = Pool::new();
pool.skill("bash", BASH_SKILL);
let wt = worktree();
fs::create_dir_all(wt.path().join("descriptions/skills/bash.md")).unwrap();
let err = snapshot(pool.root(), wt.path()).unwrap_err();
assert!(matches!(err, Error::Io { .. }), "got {err:?}");
}
#[test]
fn unreadable_pool_dir_surfaces_io_error() {
let holder = TempDir::new().unwrap();
let data_root = holder.path().join("root");
fs::create_dir_all(&data_root).unwrap();
fs::write(data_root.join(TOOLS_SUBDIR), b"not a dir").unwrap();
let wt = worktree();
let err = snapshot(&data_root, wt.path()).unwrap_err();
assert!(matches!(err, Error::Io { .. }), "got {err:?}");
}