use clap::Command;
use skill_embed::{Error, File, Installer, SkillSet, State};
fn skills() -> Installer {
let set = SkillSet::from_files([
File::new(
"demo-skill/SKILL.md",
b"---\nname: demo-skill\ndescription: A skill this test installs.\n---\n\n# Demo\n"
.to_vec(),
),
File::new("demo-skill/scripts/run.sh", b"#!/bin/sh\necho hi\n".to_vec()),
])
.expect("the fixture is a valid skill set");
Installer::new(set).with_tool_name("mytool").with_version("v1.0.0")
}
fn matches(skills: &Installer, args: &[&str]) -> clap::ArgMatches {
let root = Command::new("mytool").subcommand(skill_embed_clap::command(skills));
let all = root
.try_get_matches_from(std::iter::once("mytool").chain(args.iter().copied()))
.expect("the arguments parse");
all.subcommand_matches("skill").expect("the skill command").clone()
}
fn sub(skills: &Installer, args: &[&str]) -> clap::ArgMatches {
matches(skills, args).subcommand().expect("a subcommand").1.clone()
}
#[test]
fn install_list_and_uninstall_reach_the_same_directory() {
let tmp = tempfile::tempdir().expect("a temporary directory");
let dir = tmp.path().join("skills");
let dir_arg = dir.display().to_string();
let skills = skills();
skill_embed_clap::run(&skills, &matches(&skills, &["skill", "install", "--dir", &dir_arg]))
.expect("the install");
assert!(dir.join("demo-skill/SKILL.md").exists());
let options = skill_embed_clap::options(&sub(&skills, &["skill", "list", "--dir", &dir_arg]))
.expect("the options");
let statuses = skills.status(&options).expect("status");
assert_eq!(statuses[0].state, State::UpToDate);
skill_embed_clap::run(&skills, &matches(&skills, &["skill", "remove", "--dir", &dir_arg]))
.expect("the alias");
assert!(!dir.join("demo-skill").exists());
}
#[test]
fn the_flags_are_the_ones_gh_skill_install_defines() {
let skills = skills();
let options = skill_embed_clap::options(&sub(
&skills,
&[
"skill",
"install",
"--agent",
"claude-code,cursor",
"--scope",
"user",
"-f",
"--dry-run",
"demo-skill",
],
))
.expect("the options");
assert_eq!(options.agents.len(), 2);
assert_eq!(options.scope, Some(skill_embed::Scope::User));
assert!(options.force);
assert!(options.dry_run);
assert_eq!(options.names, ["demo-skill"]);
}
#[test]
fn a_flag_after_a_name_is_still_a_flag() {
let skills = skills();
let options =
skill_embed_clap::options(&sub(&skills, &["skill", "install", "demo-skill", "--dry-run"]))
.expect("the options");
assert!(options.dry_run);
assert_eq!(options.names, ["demo-skill"]);
}
#[test]
fn the_default_agents_come_from_the_installer() {
let skills = skills();
let options =
skill_embed_clap::options(&sub(&skills, &["skill", "list"])).expect("the options");
assert_eq!(options.agents, [skill_embed::AgentSelector::Detected]);
let copilot = skills.with_default_agents([skill_embed::AgentSelector::from(
&skill_embed::Agent::GITHUB_COPILOT,
)]);
let options =
skill_embed_clap::options(&sub(&copilot, &["skill", "list"])).expect("the options");
assert_eq!(options.agents, [skill_embed::AgentSelector::Named("github-copilot".to_owned())]);
}
#[test]
fn a_scope_clap_does_not_know_never_reaches_the_core() {
let skills = skills();
let root = Command::new("mytool").subcommand(skill_embed_clap::command(&skills));
let err = root
.try_get_matches_from(["mytool", "skill", "install", "--scope", "nowhere"])
.expect_err("an unknown scope was accepted");
assert_eq!(err.kind(), clap::error::ErrorKind::InvalidValue);
}
#[test]
fn a_blocked_destination_is_reported_and_refused() {
let tmp = tempfile::tempdir().expect("a temporary directory");
let dir = tmp.path().join("skills");
std::fs::create_dir_all(dir.join("demo-skill")).expect("a directory");
std::fs::write(dir.join("demo-skill").join(skill_embed::SKILL_FILE), "hand written\n")
.expect("a manifest");
let skills = skills();
let outcome = skill_embed_clap::run(
&skills,
&matches(&skills, &["skill", "install", "--dir", &dir.display().to_string()]),
);
assert!(matches!(outcome, Err(Error::NeedsForce(_))), "{outcome:?}");
assert_eq!(
std::fs::read_to_string(dir.join("demo-skill").join(skill_embed::SKILL_FILE))
.expect("the manifest"),
"hand written\n"
);
}
#[test]
fn the_help_names_the_tool_and_the_agents() {
let skills = skills();
let mut command = skill_embed_clap::command(&skills);
let overview = command.render_long_help().to_string();
assert!(overview.contains("Manage the agent skills embedded in mytool"), "{overview}");
assert!(overview.contains("[alias: remove]"), "{overview}");
let install = command
.find_subcommand_mut("install")
.expect("the install subcommand")
.render_long_help()
.to_string();
assert!(install.contains("--agent"), "{install}");
assert!(install.contains("github-copilot|claude-code"), "{install}");
assert!(install.contains("[default: detected]"), "{install}");
}
#[test]
fn the_report_goes_to_the_installers_own_writer() {
#[derive(Clone, Default)]
struct Captured(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
impl std::io::Write for Captured {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().expect("the buffer").extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let tmp = tempfile::tempdir().expect("a temporary directory");
let dir = tmp.path().join("skills").display().to_string();
let out = Captured::default();
let skills = skills().with_output(out.clone());
skill_embed_clap::run(&skills, &matches(&skills, &["skill", "install", "--dir", &dir]))
.expect("the install");
let text = String::from_utf8(out.0.lock().expect("the buffer").clone()).expect("UTF-8");
assert!(text.starts_with("installed demo-skill"), "{text}");
}
#[test]
fn an_empty_agent_is_refused_here_too() {
let skills = skills();
let options = skill_embed_clap::options(&sub(&skills, &["skill", "install", "--agent", ""]));
assert!(matches!(options, Err(Error::NoAgentSelected)), "{options:?}");
}