use std::fmt;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
pub use zeph_common::SkillTrustLevel;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum SkillSource {
#[default]
Local,
Hub { url: String },
File { path: PathBuf },
}
impl fmt::Display for SkillSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Local => f.write_str("local"),
Self::Hub { url } => write!(f, "hub({url})"),
Self::File { path } => write!(f, "file({})", path.display()),
}
}
}
#[derive(Debug, Clone)]
pub struct SkillTrust {
pub skill_name: String,
pub trust_level: SkillTrustLevel,
pub source: SkillSource,
pub blake3_hash: String,
}
pub fn compute_skill_hash(skill_dir: &Path) -> std::io::Result<String> {
let content = std::fs::read(skill_dir.join("SKILL.md"))?;
Ok(blake3::hash(&content).to_hex().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display() {
assert_eq!(SkillSource::Local.to_string(), "local");
assert_eq!(
SkillSource::Hub {
url: "https://example.com".into()
}
.to_string(),
"hub(https://example.com)"
);
}
#[test]
fn compute_hash() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("SKILL.md"), "test content").unwrap();
let hash = compute_skill_hash(dir.path()).unwrap();
assert_eq!(hash.len(), 64); let hash2 = compute_skill_hash(dir.path()).unwrap();
assert_eq!(hash, hash2);
}
#[test]
fn compute_hash_different_content() {
let dir1 = tempfile::tempdir().unwrap();
let dir2 = tempfile::tempdir().unwrap();
std::fs::write(dir1.path().join("SKILL.md"), "content a").unwrap();
std::fs::write(dir2.path().join("SKILL.md"), "content b").unwrap();
let h1 = compute_skill_hash(dir1.path()).unwrap();
let h2 = compute_skill_hash(dir2.path()).unwrap();
assert_ne!(h1, h2);
}
#[test]
fn source_serde_roundtrip() {
let source = SkillSource::Hub {
url: "https://hub.example.com/skill".into(),
};
let json = serde_json::to_string(&source).unwrap();
let back: SkillSource = serde_json::from_str(&json).unwrap();
assert_eq!(back, source);
}
#[test]
fn display_file_source() {
let source = SkillSource::File {
path: std::path::PathBuf::from("/tmp/my-skill"),
};
assert_eq!(source.to_string(), "file(/tmp/my-skill)");
}
#[test]
fn display_local_source() {
assert_eq!(SkillSource::Local.to_string(), "local");
}
#[test]
fn compute_hash_missing_skill_md_returns_error() {
let dir = tempfile::tempdir().unwrap();
let result = compute_skill_hash(dir.path());
assert!(result.is_err());
}
#[test]
fn trust_level_reexport_accessible() {
let level: SkillTrustLevel = SkillTrustLevel::default();
assert_eq!(level, SkillTrustLevel::Quarantined);
assert!(level.is_active());
}
#[test]
fn source_default_is_local() {
assert_eq!(SkillSource::default(), SkillSource::Local);
}
#[test]
fn source_file_serde_roundtrip() {
let source = SkillSource::File {
path: std::path::PathBuf::from("/skills/my_skill"),
};
let json = serde_json::to_string(&source).unwrap();
let back: SkillSource = serde_json::from_str(&json).unwrap();
assert_eq!(back, source);
}
}