use std::path::Path;
use zenith_cli::commands::plugin::agent::{Agent, Scope, SkillFormat};
use zenith_cli::commands::plugin::assets::{
COMMAND_FILES, SKILL_FILES, skill_description, skill_md_body, skill_md_raw,
};
use zenith_cli::commands::plugin::detect::is_installed;
use zenith_cli::commands::plugin::install::{WriteOutcome, install_agent};
use zenith_cli::commands::plugin::paths::{SkillTarget, command_dir, skill_target};
use zenith_cli::commands::plugin::uninstall::{RemoveOutcome, uninstall_agent};
#[test]
fn skill_tree_is_embedded() {
assert!(!SKILL_FILES.is_empty(), "skill tree must be embedded");
assert!(
SKILL_FILES.iter().any(|(name, _)| *name == "SKILL.md"),
"SKILL.md must be present"
);
assert!(
SKILL_FILES
.iter()
.any(|(n, _)| n.starts_with("references/")),
"reference packs must be present"
);
assert!(!COMMAND_FILES.is_empty(), "commands must be embedded");
}
#[test]
fn frontmatter_is_stripped_for_rule_body() {
assert!(skill_md_raw().contains("# Zenith"));
assert!(
!skill_md_body().starts_with("---"),
"body has frontmatter removed"
);
assert!(skill_md_body().contains("# Zenith"));
let desc = skill_description().expect("description parsed from frontmatter");
assert!(desc.contains(".zen"));
}
#[test]
fn folder_agents_get_a_skill_directory() {
let root = Path::new("/proj");
for agent in [Agent::ClaudeCode, Agent::Codex, Agent::OpenCode] {
assert_eq!(agent.format(), SkillFormat::Folder);
match skill_target(agent, Scope::Project, root) {
Some(SkillTarget::Folder(dir)) => {
assert!(dir.ends_with("skills/zenith"), "{dir:?}");
assert!(dir.starts_with("/proj"));
}
other => panic!("{agent:?} should be a folder target, got {other:?}"),
}
}
}
#[test]
fn rule_agents_get_a_single_file() {
let root = Path::new("/proj");
match skill_target(Agent::Cursor, Scope::Project, root) {
Some(SkillTarget::Rule(p)) => assert!(p.ends_with(".cursor/rules/zenith.mdc"), "{p:?}"),
other => panic!("cursor should be a rule file, got {other:?}"),
}
}
#[test]
fn cursor_and_windsurf_have_no_user_scope() {
let root = Path::new("/proj");
assert!(skill_target(Agent::Cursor, Scope::User, root).is_none());
assert!(skill_target(Agent::Windsurf, Scope::User, root).is_none());
}
#[test]
fn only_claude_and_opencode_have_command_dirs() {
let root = Path::new("/proj");
assert!(command_dir(Agent::ClaudeCode, Scope::Project, root).is_some());
assert!(command_dir(Agent::OpenCode, Scope::Project, root).is_some());
assert!(command_dir(Agent::Codex, Scope::Project, root).is_none());
assert!(command_dir(Agent::Cursor, Scope::Project, root).is_none());
}
#[test]
fn agent_parse_accepts_aliases() {
assert_eq!(Agent::parse("claude"), Some(Agent::ClaudeCode));
assert_eq!(Agent::parse("claude-code"), Some(Agent::ClaudeCode));
assert_eq!(Agent::parse("open-code"), Some(Agent::OpenCode));
assert_eq!(Agent::parse("nope"), None);
}
#[test]
fn folder_install_writes_whole_tree_and_commands() {
let tmp = tempfile::tempdir().expect("tempdir");
let report = install_agent(Agent::ClaudeCode, Scope::Project, tmp.path(), false, false);
assert!(report.unsupported.is_none());
assert_eq!(report.files.len(), SKILL_FILES.len() + COMMAND_FILES.len());
assert!(
report
.files
.iter()
.all(|f| matches!(f.outcome, Ok(WriteOutcome::Installed)))
);
let skill = tmp.path().join(".claude/skills/zenith/SKILL.md");
let refs = tmp
.path()
.join(".claude/skills/zenith/references/layout.md");
let cmd = tmp.path().join(".claude/commands/zenith-new.md");
assert!(skill.is_file() && refs.is_file() && cmd.is_file());
assert!(is_installed(Agent::ClaudeCode, Scope::Project, tmp.path()));
}
#[test]
fn second_install_is_idempotent() {
let tmp = tempfile::tempdir().expect("tempdir");
install_agent(Agent::OpenCode, Scope::Project, tmp.path(), false, false);
let again = install_agent(Agent::OpenCode, Scope::Project, tmp.path(), false, false);
assert!(
again
.files
.iter()
.all(|f| matches!(f.outcome, Ok(WriteOutcome::AlreadyCurrent)))
);
}
#[test]
fn changed_file_needs_force() {
let tmp = tempfile::tempdir().expect("tempdir");
install_agent(Agent::ClaudeCode, Scope::Project, tmp.path(), false, false);
let skill = tmp.path().join(".claude/skills/zenith/SKILL.md");
std::fs::write(&skill, b"tampered").expect("write");
let no_force = install_agent(Agent::ClaudeCode, Scope::Project, tmp.path(), false, false);
assert!(
no_force
.files
.iter()
.any(|f| matches!(f.outcome, Ok(WriteOutcome::WouldOverwrite)))
);
assert_eq!(std::fs::read(&skill).unwrap(), b"tampered");
install_agent(Agent::ClaudeCode, Scope::Project, tmp.path(), true, false);
assert_eq!(std::fs::read_to_string(&skill).unwrap(), skill_md_raw());
}
#[test]
fn dry_run_writes_nothing() {
let tmp = tempfile::tempdir().expect("tempdir");
let report = install_agent(Agent::ClaudeCode, Scope::Project, tmp.path(), false, true);
assert!(
report
.files
.iter()
.all(|f| matches!(f.outcome, Ok(WriteOutcome::Installed)))
);
assert!(
!tmp.path().join(".claude").exists(),
"dry-run must not write"
);
}
#[test]
fn rule_install_writes_one_rendered_file() {
let tmp = tempfile::tempdir().expect("tempdir");
let report = install_agent(Agent::Cursor, Scope::Project, tmp.path(), false, false);
assert_eq!(report.files.len(), 1);
let mdc = tmp.path().join(".cursor/rules/zenith.mdc");
let body = std::fs::read_to_string(&mdc).expect("read mdc");
assert!(body.starts_with("---\nalwaysApply: false"));
assert!(body.contains("Single-file install"));
}
#[test]
fn uninstall_removes_tree_and_commands() {
let tmp = tempfile::tempdir().expect("tempdir");
install_agent(Agent::ClaudeCode, Scope::Project, tmp.path(), false, false);
let report = uninstall_agent(Agent::ClaudeCode, Scope::Project, tmp.path(), false);
assert!(
report
.items
.iter()
.all(|i| matches!(i.outcome, Ok(RemoveOutcome::Removed)))
);
assert!(!tmp.path().join(".claude/skills/zenith").exists());
assert!(!is_installed(Agent::ClaudeCode, Scope::Project, tmp.path()));
}
#[test]
fn uninstall_absent_is_clean() {
let tmp = tempfile::tempdir().expect("tempdir");
let report = uninstall_agent(Agent::Zed, Scope::Project, tmp.path(), false);
assert!(
report
.items
.iter()
.all(|i| matches!(i.outcome, Ok(RemoveOutcome::Absent)))
);
}