microclaw_engine/internal/clawhub/
lockfile.rs1use crate::internal::clawhub::types::LockFile;
2use microclaw_core::error::MicroClawError;
3use std::collections::HashMap;
4use std::path::Path;
5
6pub fn read_lockfile(path: &Path) -> Result<LockFile, MicroClawError> {
8 if !path.exists() {
9 return Ok(LockFile {
10 version: 1,
11 skills: HashMap::new(),
12 });
13 }
14 let content = std::fs::read_to_string(path)
15 .map_err(|e| MicroClawError::Config(format!("Failed to read lockfile: {}", e)))?;
16 let lock: LockFile = serde_json::from_str(&content)
17 .map_err(|e| MicroClawError::Config(format!("Failed to parse lockfile: {}", e)))?;
18 Ok(lock)
19}
20
21pub fn write_lockfile(path: &Path, lock: &LockFile) -> Result<(), MicroClawError> {
23 if let Some(parent) = path.parent() {
24 std::fs::create_dir_all(parent)
25 .map_err(|e| MicroClawError::Config(format!("Failed to create lockfile dir: {}", e)))?;
26 }
27 let content = serde_json::to_string_pretty(lock)
28 .map_err(|e| MicroClawError::Config(format!("Failed to serialize lockfile: {}", e)))?;
29 std::fs::write(path, content)
30 .map_err(|e| MicroClawError::Config(format!("Failed to write lockfile: {}", e)))?;
31 Ok(())
32}
33
34pub fn is_clawhub_managed(lock: &LockFile, slug: &str) -> bool {
36 lock.skills.contains_key(slug)
37}
38
39#[cfg(test)]
40mod tests {
41 use super::*;
42 use crate::internal::clawhub::types::LockEntry;
43
44 #[test]
45 fn test_lockfile_missing_returns_empty() {
46 let temp_path = std::env::temp_dir().join(format!("nonexistent_{}", uuid::Uuid::new_v4()));
47 let lock = read_lockfile(&temp_path).unwrap();
48 assert_eq!(lock.version, 1);
49 assert!(lock.skills.is_empty());
50 }
51
52 #[test]
53 fn test_lockfile_roundtrip() {
54 let temp_dir = std::env::temp_dir().join(format!("clawhub_test_{}", uuid::Uuid::new_v4()));
55 std::fs::create_dir_all(&temp_dir).unwrap();
56 let lock_path = temp_dir.join("clawhub.lock.json");
57
58 let mut lock = LockFile {
59 version: 1,
60 skills: HashMap::new(),
61 };
62 lock.skills.insert(
63 "test-skill".into(),
64 LockEntry {
65 slug: "test-skill".into(),
66 installed_version: "1.0.0".into(),
67 installed_at: "2026-02-18T00:00:00Z".into(),
68 content_hash: "sha256:abc".into(),
69 tree_hash: None,
70 local_path: "/tmp/test".into(),
71 },
72 );
73
74 write_lockfile(&lock_path, &lock).unwrap();
75 let read = read_lockfile(&lock_path).unwrap();
76 assert_eq!(
77 read.skills.get("test-skill").unwrap().installed_version,
78 "1.0.0"
79 );
80
81 std::fs::remove_dir_all(&temp_dir).ok();
82 }
83
84 #[test]
85 fn test_is_clawhub_managed() {
86 let mut lock = LockFile {
87 version: 1,
88 skills: HashMap::new(),
89 };
90 lock.skills.insert(
91 "my-skill".into(),
92 LockEntry {
93 slug: "my-skill".into(),
94 installed_version: "1.0.0".into(),
95 installed_at: "2026-02-18T00:00:00Z".into(),
96 content_hash: "sha256:abc".into(),
97 tree_hash: None,
98 local_path: "/tmp/test".into(),
99 },
100 );
101
102 assert!(is_clawhub_managed(&lock, "my-skill"));
103 assert!(!is_clawhub_managed(&lock, "other-skill"));
104 }
105}