use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use beck::agents::adapter::Adapter;
use beck::agents::claude_code::ClaudeCodeAdapter;
use beck::agents::manifest::InstallMode;
use beck::agents::skill::Skill;
static ENV_LOCK: Mutex<()> = Mutex::new(());
struct HomeGuard {
previous: Option<std::ffi::OsString>,
}
impl HomeGuard {
fn set(value: &Path) -> Self {
let previous = std::env::var_os("HOME");
unsafe { std::env::set_var("HOME", value) };
Self { previous }
}
}
impl Drop for HomeGuard {
fn drop(&mut self) {
match &self.previous {
Some(v) => unsafe { std::env::set_var("HOME", v) },
None => unsafe { std::env::remove_var("HOME") },
}
}
}
fn tempdir(name: &str) -> PathBuf {
let base = std::env::temp_dir().join(format!("beck-cc-e2e-{name}"));
let _ = fs::remove_dir_all(&base);
fs::create_dir_all(&base).unwrap();
base
}
#[test]
fn install_then_read_through_symlink_then_uninstall() {
let _lock = ENV_LOCK.lock().unwrap();
let root = tempdir("round-trip");
let home = root.join("home");
let claude = home.join(".claude");
fs::create_dir_all(&claude).unwrap();
let skills_home = home.join("beck").join("skills");
let skill_dir = skills_home.join("caveman");
fs::create_dir_all(&skill_dir).unwrap();
let source = skill_dir.join("SKILL.md");
let body = "---\nname: caveman\ndescription: cc integration sample\n---\ncontent of caveman\n";
fs::write(&source, body).unwrap();
let _guard = HomeGuard::set(&home);
let skills = Skill::discover_in(&skills_home).unwrap();
assert_eq!(skills.len(), 1, "expected one skill in fake home");
let skill = &skills[0];
assert_eq!(skill.name, "caveman");
let adapter = ClaudeCodeAdapter::new();
assert!(adapter.detect(), "detect() should see the .claude dir");
let plan = adapter.plan(skill).expect("plan");
let expected_target = claude
.join("skills")
.join("caveman")
.join("SKILL.md");
assert_eq!(plan.target, expected_target);
assert_eq!(plan.mode, InstallMode::Symlink);
assert!(plan.transform.is_none());
let entry = adapter.install(&plan).expect("install");
assert_eq!(entry.skill, "caveman");
assert_eq!(entry.agent, "claude-code");
assert_eq!(entry.target, expected_target);
assert_eq!(entry.mode, InstallMode::Symlink);
assert_eq!(entry.sha256.len(), 64);
assert!(entry.installed_at.ends_with('Z'));
let meta = fs::symlink_metadata(&expected_target).unwrap();
assert!(meta.file_type().is_symlink());
let through = fs::read_to_string(&expected_target).unwrap();
assert_eq!(through, body);
let entry2 = adapter.install(&plan).expect("idempotent");
assert_eq!(entry2.skill, entry.skill);
assert_eq!(entry2.target, entry.target);
assert_eq!(entry2.sha256, entry.sha256);
adapter.uninstall(&entry).expect("uninstall");
assert!(
!expected_target.exists(),
"symlink should be removed after uninstall"
);
assert!(source.exists());
assert_eq!(fs::read_to_string(&source).unwrap(), body);
}