use std::fs;
use std::path::{Path, PathBuf};
use assert_cmd::Command;
use predicates::prelude::*;
fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf()
}
fn copy_fixture(name: &str) -> PathBuf {
let src = repo_root().join("tests/fixtures/repos").join(name);
let dest = tempfile::tempdir().unwrap().keep();
copy_dir(&src, &dest);
dest
}
fn copy_dir(src: &Path, dest: &Path) {
fs::create_dir_all(dest).unwrap();
for entry in fs::read_dir(src).unwrap() {
let entry = entry.unwrap();
let from = entry.path();
let to = dest.join(entry.file_name());
if from.is_dir() {
copy_dir(&from, &to);
} else {
fs::copy(&from, &to).unwrap();
}
}
}
fn write_skillpack_toml(root: &Path, name: &str) {
let toml = format!(
"[skill]\n\
name = \"{name}\"\n\
one_line_description = \"Print a journal entry to stdout\"\n\
when_to_use_phrases = [\"log a journal entry\", \"record a quick note\"]\n\
invocation_command = \"{name} --new \\\"entry\\\"\"\n\
license = \"MIT\"\n"
);
fs::write(root.join("skillpack.toml"), toml).unwrap();
}
#[test]
fn rust_cli_init_then_verify_round_trip() {
let root = copy_fixture("rust-cli");
Command::new("cargo")
.args(["build", "--quiet"])
.current_dir(&root)
.assert()
.success();
write_skillpack_toml(&root, "sample-rust");
Command::cargo_bin("skillpack")
.unwrap()
.args([
"init",
"--root",
".",
"--non-interactive",
"--accept-warnings",
])
.current_dir(&root)
.assert()
.success();
assert!(root.join(".claude-plugin/marketplace.json").exists());
assert!(root.join(".claude-plugin/plugin.json").exists());
assert!(root.join("skills/sample-rust/SKILL.md").exists());
assert!(root.join("skillpack.toml").exists());
Command::cargo_bin("skillpack")
.unwrap()
.args(["verify", "--root", "."])
.current_dir(&root)
.assert()
.success()
.stdout(predicate::str::contains("verify OK").or(predicate::str::contains("0 failed")));
let mp = fs::read_to_string(root.join(".claude-plugin/marketplace.json")).unwrap();
let v: serde_json::Value = serde_json::from_str(&mp).unwrap();
assert_eq!(v["plugins"][0]["source"], "./");
assert_eq!(v["plugins"][0]["name"], "sample-rust");
assert_eq!(v["name"], "sample-rust");
let pj = fs::read_to_string(root.join(".claude-plugin/plugin.json")).unwrap();
let v: serde_json::Value = serde_json::from_str(&pj).unwrap();
assert_eq!(v["name"], "sample-rust");
assert_eq!(v["license"], "MIT");
}
#[test]
fn pure_library_init_skips_invocation_and_writes_import_pattern() {
let root = copy_fixture("node-lib");
let toml = "[skill]\n\
name = \"sample-lib\"\n\
one_line_description = \"Parse CSV files with a small library\"\n\
when_to_use_phrases = [\"ingest csv\", \"convert rows\"]\n\
import_pattern = \"import { parse } from 'sample-lib'\"\n\
license = \"MIT\"\n";
fs::write(root.join("skillpack.toml"), toml).unwrap();
Command::cargo_bin("skillpack")
.unwrap()
.args([
"init",
"--root",
".",
"--non-interactive",
"--accept-warnings",
])
.current_dir(&root)
.assert()
.success();
let skill = fs::read_to_string(root.join("skills/sample-lib/SKILL.md")).unwrap();
assert!(skill.contains("import { parse } from 'sample-lib'"));
assert!(!skill.contains("## Invocation"));
assert!(skill.contains("## Usage"));
Command::cargo_bin("skillpack")
.unwrap()
.args(["verify", "--root", "."])
.current_dir(&root)
.assert()
.success()
.stdout(predicate::str::contains("Skipped: pure-library project"));
}
#[test]
fn non_interactive_without_skillpack_toml_refuses_to_write() {
let root = copy_fixture("rust-cli");
Command::new("cargo")
.args(["build", "--quiet"])
.current_dir(&root)
.assert()
.success();
Command::cargo_bin("skillpack")
.unwrap()
.args(["init", "--root", ".", "--non-interactive"])
.current_dir(&root)
.assert()
.failure() .stderr(predicate::str::contains("no skillpack.toml found"));
assert!(!root.join(".claude-plugin").exists());
}
#[test]
fn broken_cli_verify_flags_drift() {
let root = copy_fixture("broken-cli");
Command::new("cargo")
.args(["build", "--quiet"])
.current_dir(&root)
.assert()
.success();
let out = Command::cargo_bin("skillpack")
.unwrap()
.args(["verify", "--root", "."])
.current_dir(&root)
.assert()
.failure()
.get_output()
.stdout
.clone();
let s = String::from_utf8_lossy(&out);
assert!(
s.contains("flag_drift") || s.contains("missing from `--help`"),
"expected a flag-drift failure, got:\n{s}"
);
}
#[test]
fn init_with_empty_when_to_use_warns_on_verify() {
let root = copy_fixture("rust-cli");
Command::new("cargo")
.args(["build", "--quiet"])
.current_dir(&root)
.assert()
.success();
let toml = "[skill]\n\
name = \"sample-rust\"\n\
one_line_description = \"Print a journal entry to stdout\"\n\
when_to_use_phrases = []\n\
invocation_command = \"sample-rust --new \\\"entry\\\"\"\n\
license = \"MIT\"\n";
fs::write(root.join("skillpack.toml"), toml).unwrap();
Command::cargo_bin("skillpack")
.unwrap()
.args([
"init",
"--root",
".",
"--non-interactive",
"--accept-warnings",
])
.current_dir(&root)
.assert()
.success();
let skill = fs::read_to_string(root.join("skills/sample-rust/SKILL.md")).unwrap();
assert!(skill.contains("when_to_use: \"\""));
assert!(!skill.contains("(unspecified)"));
let out = Command::cargo_bin("skillpack")
.unwrap()
.args(["verify", "--root", "."])
.current_dir(&root)
.assert()
.success()
.get_output()
.stdout
.clone();
let s = String::from_utf8_lossy(&out);
assert!(
s.contains("when_to_use") && s.contains("warn"),
"expected a when_to_use warning, got:\n{s}"
);
}
#[test]
fn verify_format_json_is_machine_readable() {
let root = copy_fixture("rust-cli");
Command::new("cargo")
.args(["build", "--quiet"])
.current_dir(&root)
.assert()
.success();
write_skillpack_toml(&root, "sample-rust");
Command::cargo_bin("skillpack")
.unwrap()
.args([
"init",
"--root",
".",
"--non-interactive",
"--accept-warnings",
])
.current_dir(&root)
.assert()
.success();
let out = Command::cargo_bin("skillpack")
.unwrap()
.args(["verify", "--root", ".", "--format", "json"])
.current_dir(&root)
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_str(&String::from_utf8_lossy(&out)).unwrap();
assert_eq!(v["ok"], serde_json::Value::Bool(true));
let results = v["results"].as_array().unwrap();
assert!(results.iter().all(|r| r["check_id"].is_string()));
assert!(results
.iter()
.any(|r| r["check_id"].as_str().unwrap().starts_with("invocation.")));
}
#[test]
fn verify_checks_all_skills_in_a_multi_skill_plugin() {
let root = copy_fixture("rust-cli");
Command::new("cargo")
.args(["build", "--quiet"])
.current_dir(&root)
.assert()
.success();
write_skillpack_toml(&root, "sample-rust");
Command::cargo_bin("skillpack")
.unwrap()
.args([
"init",
"--root",
".",
"--non-interactive",
"--accept-warnings",
])
.current_dir(&root)
.assert()
.success();
fs::create_dir_all(root.join("skills/second-tool")).unwrap();
fs::write(
root.join("skills/second-tool/SKILL.md"),
"---\nname: second-tool\ndescription: \"\"\nwhen_to_use: \"x\"\n---\nbody\n",
)
.unwrap();
let out = Command::cargo_bin("skillpack")
.unwrap()
.args(["verify", "--root", "."])
.current_dir(&root)
.assert()
.failure()
.get_output()
.stdout
.clone();
let s = String::from_utf8_lossy(&out);
assert!(
s.contains("skills/second-tool/SKILL.md"),
"expected the second skill's path in the report, got:\n{s}"
);
}
#[test]
fn reverse_drift_warns_on_success_path_via_verify_run() {
let root = copy_fixture("rust-cli");
Command::new("cargo")
.args(["build", "--quiet"])
.current_dir(&root)
.assert()
.success();
fs::create_dir_all(root.join(".claude-plugin")).unwrap();
fs::create_dir_all(root.join("skills/sample-rust")).unwrap();
fs::write(
root.join(".claude-plugin/marketplace.json"),
"{\"name\":\"mp\",\"owner\":{\"name\":\"x\"},\"plugins\":[{\"name\":\"sample-rust\",\"source\":\"./\"}]}",
)
.unwrap();
fs::write(
root.join(".claude-plugin/plugin.json"),
"{\"name\":\"sample-rust\",\"description\":\"Do thing\"}",
)
.unwrap();
fs::write(
root.join("skills/sample-rust/SKILL.md"),
"---\nname: sample-rust\ndescription: \"Run the sample rust thing\"\nwhen_to_use: \"run sample rust\"\n---\n\n## Invocation\n\n```\nsample-rust --new entry\n```\n",
)
.unwrap();
let out = Command::cargo_bin("skillpack")
.unwrap()
.args(["verify", "--root", "."])
.current_dir(&root)
.assert()
.success() .get_output()
.stdout
.clone();
let s = String::from_utf8_lossy(&out);
assert!(
s.contains("undocumented_flags") || s.contains("--verbose"),
"expected reverse-drift warning for --verbose via verify, got:\n{s}"
);
}
#[test]
fn multi_cli_plugin_warns_invocation_only_checked_first() {
let root = copy_fixture("rust-cli");
Command::new("cargo")
.args(["build", "--quiet"])
.current_dir(&root)
.assert()
.success();
fs::create_dir_all(root.join(".claude-plugin")).unwrap();
fs::create_dir_all(root.join("skills/sample-rust")).unwrap();
fs::write(
root.join(".claude-plugin/marketplace.json"),
"{\"name\":\"mp\",\"owner\":{\"name\":\"x\"},\"plugins\":[{\"name\":\"sample-rust\",\"source\":\"./\"}]}",
)
.unwrap();
fs::write(
root.join(".claude-plugin/plugin.json"),
"{\"name\":\"sample-rust\",\"description\":\"Do thing\"}",
)
.unwrap();
fs::write(
root.join("skills/sample-rust/SKILL.md"),
"---\nname: sample-rust\ndescription: \"Run the sample rust thing\"\nwhen_to_use: \"run sample rust\"\n---\n\n## Invocation\n\n```\nsample-rust --new entry\n```\n",
)
.unwrap();
fs::create_dir_all(root.join("skills/zzz-other-tool")).unwrap();
fs::write(
root.join("skills/zzz-other-tool/SKILL.md"),
"---\nname: zzz-other-tool\ndescription: \"Run the other thing\"\nwhen_to_use: \"run other\"\n---\n\n## Invocation\n\n```\nzzz-other-tool --flag\n```\n",
)
.unwrap();
let out = Command::cargo_bin("skillpack")
.unwrap()
.args(["verify", "--root", "."])
.current_dir(&root)
.assert()
.success() .get_output()
.stdout
.clone();
let s = String::from_utf8_lossy(&out);
assert!(
s.contains("invocation.multi_cli") || s.contains("only run against the first"),
"expected a multi-CLI invocation warning, got:\n{s}"
);
}
#[test]
fn hand_written_pack_documenting_unrunnable_cli_warns() {
let dest = tempfile::tempdir().unwrap().keep();
fs::create_dir_all(dest.join(".claude-plugin")).unwrap();
fs::create_dir_all(dest.join("skills/foo")).unwrap();
fs::write(
dest.join(".claude-plugin/marketplace.json"),
"{\"name\":\"foo-marketplace\",\"owner\":{\"name\":\"x\"},\"plugins\":[{\"name\":\"foo\",\"source\":\"./\"}]}",
)
.unwrap();
fs::write(
dest.join(".claude-plugin/plugin.json"),
"{\"name\":\"foo\",\"description\":\"Do thing\"}",
)
.unwrap();
fs::write(
dest.join("skills/foo/SKILL.md"),
"---\nname: foo\ndescription: \"Run the foo thing\"\nwhen_to_use: \"run foo\"\n---\n\n## Invocation\n\n```\nfoo --new entry\n```\n",
)
.unwrap();
let out = Command::cargo_bin("skillpack")
.unwrap()
.args(["verify", "--root", "."])
.current_dir(&dest)
.assert()
.success() .get_output()
.stdout
.clone();
let s = String::from_utf8_lossy(&out);
assert!(
s.contains("not_runnable_here") || s.contains("no runnable command"),
"expected a not-runnable warning, got:\n{s}"
);
assert!(
!s.contains("pure-library project"),
"a documented CLI must not read as a pure library, got:\n{s}"
);
}
#[test]
fn init_critical_decline_exits_fixable_not_aborted() {
let root = copy_fixture("bad-help");
Command::new("cargo")
.args(["build", "--quiet"])
.current_dir(&root)
.assert()
.success();
write_skillpack_toml(&root, "bad-help");
let out = Command::cargo_bin("skillpack")
.unwrap()
.args(["init", "--root", "."])
.arg("--accept-warnings") .current_dir(&root)
.write_stdin("n\n")
.assert()
.get_output()
.clone();
let code = out.status.code().unwrap_or(-1);
assert_eq!(
code, 2,
"declining a fixable critical must exit INIT_FIXABLE (2); got {code}"
);
assert!(
!root.join(".claude-plugin/marketplace.json").exists(),
"declined init must not write the marketplace manifest"
);
}
fn node_available() -> bool {
std::process::Command::new("node")
.arg("--version")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[test]
fn node_cli_init_then_verify_round_trip() {
if !node_available() {
eprintln!("skipped: node not on PATH");
return;
}
let root = copy_fixture("node-cli");
let toml = "[skill]\n\
name = \"sample-node\"\n\
one_line_description = \"Build and run a sample Node CLI\"\n\
when_to_use_phrases = [\"build a node sample\", \"run the node demo\"]\n\
invocation_command = \"sample-node --build\"\n\
license = \"MIT\"\n";
fs::write(root.join("skillpack.toml"), toml).unwrap();
Command::cargo_bin("skillpack")
.unwrap()
.args([
"init",
"--root",
".",
"--non-interactive",
"--accept-warnings",
])
.current_dir(&root)
.assert()
.success();
assert!(root.join(".claude-plugin/marketplace.json").exists());
assert!(root.join(".claude-plugin/plugin.json").exists());
assert!(root.join("skills/sample-node/SKILL.md").exists());
assert!(root.join("skillpack.toml").exists());
let skill = fs::read_to_string(root.join("skills/sample-node/SKILL.md")).unwrap();
assert!(skill.contains("## Invocation"));
assert!(!skill.contains("## Usage"));
Command::cargo_bin("skillpack")
.unwrap()
.args(["verify", "--root", "."])
.current_dir(&root)
.assert()
.success()
.stdout(predicate::str::contains("verify OK"))
.stdout(predicate::str::contains(
"documented `--help` runs and produces output",
))
.stdout(predicate::str::contains(
"every documented flag exists in `--help`",
));
let mp = fs::read_to_string(root.join(".claude-plugin/marketplace.json")).unwrap();
let v: serde_json::Value = serde_json::from_str(&mp).unwrap();
assert_eq!(v["plugins"][0]["source"], "./");
assert_eq!(v["plugins"][0]["name"], "sample-node");
}
fn python_available() -> bool {
std::process::Command::new("python3")
.arg("--version")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[test]
fn python_cli_init_then_verify_round_trip() {
if !python_available() {
eprintln!("skipped: python not on PATH");
return;
}
let root = copy_fixture("python-cli");
let toml = "[skill]\n\
name = \"sample-python\"\n\
one_line_description = \"Lint and fix a sample Python project\"\n\
when_to_use_phrases = [\"lint python code\", \"apply auto-fixes\"]\n\
invocation_command = \"sample-python --lint\"\n\
license = \"MIT\"\n";
fs::write(root.join("skillpack.toml"), toml).unwrap();
Command::cargo_bin("skillpack")
.unwrap()
.args([
"init",
"--root",
".",
"--non-interactive",
"--accept-warnings",
])
.current_dir(&root)
.assert()
.success();
assert!(root.join(".claude-plugin/marketplace.json").exists());
assert!(root.join(".claude-plugin/plugin.json").exists());
assert!(root.join("skills/sample-python/SKILL.md").exists());
assert!(root.join("skillpack.toml").exists());
let skill = fs::read_to_string(root.join("skills/sample-python/SKILL.md")).unwrap();
assert!(skill.contains("## Invocation"));
assert!(!skill.contains("## Usage"));
Command::cargo_bin("skillpack")
.unwrap()
.args(["verify", "--root", "."])
.current_dir(&root)
.assert()
.success()
.stdout(predicate::str::contains("verify OK"))
.stdout(predicate::str::contains(
"documented `--help` runs and produces output",
))
.stdout(predicate::str::contains(
"every documented flag exists in `--help`",
));
let mp = fs::read_to_string(root.join(".claude-plugin/marketplace.json")).unwrap();
let v: serde_json::Value = serde_json::from_str(&mp).unwrap();
assert_eq!(v["plugins"][0]["source"], "./");
assert_eq!(v["plugins"][0]["name"], "sample-python");
}
fn go_available() -> bool {
std::process::Command::new("go")
.arg("version")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[test]
#[ignore = "requires `go` on PATH; runs on CI via --include-ignored"]
fn go_cli_init_then_verify_round_trip() {
if !go_available() {
eprintln!("skipped: go not on PATH");
return;
}
let root = copy_fixture("go-cli");
let toml = "[skill]\n\
name = \"sample-go\"\n\
one_line_description = \"Lint and fix a sample Go project\"\n\
when_to_use_phrases = [\"lint go code\", \"run the go demo\"]\n\
invocation_command = \"sample-go --lint\"\n\
license = \"MIT\"\n";
fs::write(root.join("skillpack.toml"), toml).unwrap();
Command::cargo_bin("skillpack")
.unwrap()
.args([
"init",
"--root",
".",
"--non-interactive",
"--accept-warnings",
])
.current_dir(&root)
.assert()
.success();
assert!(root.join(".claude-plugin/marketplace.json").exists());
assert!(root.join(".claude-plugin/plugin.json").exists());
assert!(root.join("skills/sample-go/SKILL.md").exists());
assert!(root.join("skillpack.toml").exists());
let skill = fs::read_to_string(root.join("skills/sample-go/SKILL.md")).unwrap();
assert!(skill.contains("## Invocation"));
assert!(!skill.contains("## Usage"));
Command::cargo_bin("skillpack")
.unwrap()
.args(["verify", "--root", "."])
.current_dir(&root)
.assert()
.success()
.stdout(predicate::str::contains("verify OK"))
.stdout(predicate::str::contains(
"documented `--help` runs and produces output",
))
.stdout(predicate::str::contains(
"every documented flag exists in `--help`",
));
let mp = fs::read_to_string(root.join(".claude-plugin/marketplace.json")).unwrap();
let v: serde_json::Value = serde_json::from_str(&mp).unwrap();
assert_eq!(v["plugins"][0]["source"], "./");
assert_eq!(v["plugins"][0]["name"], "sample-go");
}
fn ruby_available() -> bool {
std::process::Command::new("ruby")
.arg("--version")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[test]
#[ignore = "requires `ruby` on PATH; runs on CI via --include-ignored"]
fn ruby_cli_init_then_verify_round_trip() {
if !ruby_available() {
eprintln!("skipped: ruby not on PATH");
return;
}
let root = copy_fixture("ruby-cli");
let toml = "[skill]\n\
name = \"sample-ruby\"\n\
one_line_description = \"Lint and fix a sample Ruby project\"\n\
when_to_use_phrases = [\"lint ruby code\", \"run the ruby demo\"]\n\
invocation_command = \"sample-ruby --lint\"\n\
license = \"MIT\"\n";
fs::write(root.join("skillpack.toml"), toml).unwrap();
Command::cargo_bin("skillpack")
.unwrap()
.args([
"init",
"--root",
".",
"--non-interactive",
"--accept-warnings",
])
.current_dir(&root)
.assert()
.success();
assert!(root.join(".claude-plugin/marketplace.json").exists());
assert!(root.join(".claude-plugin/plugin.json").exists());
assert!(root.join("skills/sample-ruby/SKILL.md").exists());
assert!(root.join("skillpack.toml").exists());
let skill = fs::read_to_string(root.join("skills/sample-ruby/SKILL.md")).unwrap();
assert!(skill.contains("## Invocation"));
assert!(!skill.contains("## Usage"));
Command::cargo_bin("skillpack")
.unwrap()
.args(["verify", "--root", "."])
.current_dir(&root)
.assert()
.success()
.stdout(predicate::str::contains("verify OK"))
.stdout(predicate::str::contains(
"documented `--help` runs and produces output",
))
.stdout(predicate::str::contains(
"every documented flag exists in `--help`",
));
let mp = fs::read_to_string(root.join(".claude-plugin/marketplace.json")).unwrap();
let v: serde_json::Value = serde_json::from_str(&mp).unwrap();
assert_eq!(v["plugins"][0]["source"], "./");
assert_eq!(v["plugins"][0]["name"], "sample-ruby");
}
#[test]
fn subcommand_cli_init_then_verify_round_trip() {
let root = copy_fixture("subcommand-cli");
Command::new("cargo")
.args(["build", "--quiet"])
.current_dir(&root)
.assert()
.success();
let toml = "[skill]\n\
name = \"sample-sub\"\n\
one_line_description = \"Scaffold and verify a skill pack\"\n\
when_to_use_phrases = [\"scaffold a skill pack\", \"verify a skill pack\"]\n\
invocation_command = \"sample-sub init --root\"\n\
license = \"MIT\"\n";
fs::write(root.join("skillpack.toml"), toml).unwrap();
Command::cargo_bin("skillpack")
.unwrap()
.args([
"init",
"--root",
".",
"--non-interactive",
"--accept-warnings",
])
.current_dir(&root)
.assert()
.success();
let skill = fs::read_to_string(root.join("skills/sample-sub/SKILL.md")).unwrap();
assert!(
skill.contains("### Subcommands"),
"SKILL.md must contain ### Subcommands block, got:\n{skill}"
);
assert!(
skill.contains("`init`"),
"SKILL.md must document the `init` subcommand, got:\n{skill}"
);
assert!(
skill.contains("`verify`"),
"SKILL.md must document the `verify` subcommand, got:\n{skill}"
);
assert!(
skill.contains("--non-interactive"),
"SKILL.md must list init's --non-interactive flag, got:\n{skill}"
);
assert!(
skill.contains("--format"),
"SKILL.md must list verify's --format flag, got:\n{skill}"
);
let json_out = Command::cargo_bin("skillpack")
.unwrap()
.args(["verify", "--root", ".", "--format", "json"])
.current_dir(&root)
.output()
.unwrap();
assert!(
json_out.status.success(),
"verify must exit 0, got:\n{}",
String::from_utf8_lossy(&json_out.stdout)
);
let json: serde_json::Value = serde_json::from_slice(&json_out.stdout).unwrap();
assert_eq!(
json["ok"],
serde_json::Value::Bool(true),
"verify ok must be true, got:\n{json}"
);
let sub_results: Vec<&serde_json::Value> = json["results"]
.as_array()
.unwrap()
.iter()
.filter(|r| r["check_id"] == "invocation.subcommand_drift")
.collect();
assert!(
!sub_results.is_empty(),
"verify must emit invocation.subcommand_drift results, got:\n{json}"
);
for r in &sub_results {
assert_eq!(
r["severity"], "pass",
"subcommand_drift must pass for all documented subs, got:\n{json}"
);
}
}
#[test]
fn multi_target_init_then_verify_round_trip() {
let root = copy_fixture("rust-cli");
Command::new("cargo")
.args(["build", "--quiet"])
.current_dir(&root)
.assert()
.success();
write_skillpack_toml(&root, "sample-rust");
Command::cargo_bin("skillpack")
.unwrap()
.args([
"init",
"--target",
"claude",
"--target",
"cursor",
"--target",
"codex",
"--root",
".",
"--non-interactive",
"--accept-warnings",
])
.current_dir(&root)
.assert()
.success();
assert!(root.join("skills/sample-rust/SKILL.md").exists());
assert!(root.join(".cursor/rules/sample-rust.mdc").exists());
assert!(root.join(".codex/skills/sample-rust/SKILL.md").exists());
assert!(root.join(".claude-plugin/marketplace.json").exists());
assert!(root.join(".claude-plugin/plugin.json").exists());
let json_out = Command::cargo_bin("skillpack")
.unwrap()
.args(["verify", "--root", ".", "--format", "json"])
.current_dir(&root)
.output()
.unwrap();
assert!(
json_out.status.success(),
"verify must exit 0, got:\n{}",
String::from_utf8_lossy(&json_out.stdout)
);
let json: serde_json::Value = serde_json::from_slice(&json_out.stdout).unwrap();
assert_eq!(
json["ok"],
serde_json::Value::Bool(true),
"verify ok must be true, got:\n{json}"
);
let results = json["results"].as_array().unwrap();
for (check_id, label) in [
("discovery.skill", "claude"),
("discovery.codex.skill", "codex"),
("discovery.cursor.mdc", "cursor"),
] {
let matches: Vec<&serde_json::Value> = results
.iter()
.filter(|r| r["check_id"] == check_id)
.collect();
assert!(
!matches.is_empty(),
"verify must emit {check_id} result, got:\n{json}"
);
for r in &matches {
assert_eq!(
r["severity"], "pass",
"{label} check {check_id} must pass, got:\n{json}"
);
}
}
}
#[test]
fn cursor_only_init_does_not_fail_on_missing_claude_plugin_dir() {
let root = copy_fixture("rust-cli");
Command::new("cargo")
.args(["build", "--quiet"])
.current_dir(&root)
.assert()
.success();
write_skillpack_toml(&root, "sample-rust");
Command::cargo_bin("skillpack")
.unwrap()
.args([
"init",
"--target",
"cursor",
"--root",
".",
"--non-interactive",
"--accept-warnings",
])
.current_dir(&root)
.assert()
.success();
assert!(root.join(".cursor/rules/sample-rust.mdc").exists());
assert!(!root.join(".claude-plugin").exists());
Command::cargo_bin("skillpack")
.unwrap()
.args(["verify", "--root", "."])
.current_dir(&root)
.assert()
.success()
.stdout(predicate::str::contains("verify OK"));
}
#[test]
fn broken_mdc_missing_description_fails_verify() {
let root = copy_fixture("rust-cli");
Command::new("cargo")
.args(["build", "--quiet"])
.current_dir(&root)
.assert()
.success();
write_skillpack_toml(&root, "sample-rust");
Command::cargo_bin("skillpack")
.unwrap()
.args([
"init",
"--target",
"cursor",
"--root",
".",
"--non-interactive",
"--accept-warnings",
])
.current_dir(&root)
.assert()
.success();
fs::write(
root.join(".cursor/rules/sample-rust.mdc"),
"---\nalwaysApply: false\n---\n\n# sample-rust\n",
)
.unwrap();
let out = Command::cargo_bin("skillpack")
.unwrap()
.args(["verify", "--root", ".", "--format", "json"])
.current_dir(&root)
.output()
.unwrap();
assert!(
!out.status.success(),
"verify must exit non-zero on missing description, got:\n{}",
String::from_utf8_lossy(&out.stdout)
);
let json: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
let results = json["results"].as_array().unwrap();
let name = "discovery.cursor.mdc.description";
let matches: Vec<&serde_json::Value> =
results.iter().filter(|r| r["check_id"] == name).collect();
assert!(
!matches.is_empty(),
"verify must emit {name} result, got:\n{json}"
);
for r in &matches {
assert_eq!(
r["severity"], "fail",
"{name} must be fail severity, got:\n{json}"
);
}
}
#[test]
fn self_dogfood_verify_on_repos_committed_files() {
Command::new("cargo")
.args(["build", "--release", "--quiet"])
.current_dir(repo_root())
.assert()
.success();
let out = Command::cargo_bin("skillpack")
.unwrap()
.args(["verify", "--root", "."])
.current_dir(repo_root())
.output()
.unwrap();
assert!(
out.status.success(),
"self-dogfood verify must pass, got:\n{}",
String::from_utf8_lossy(&out.stdout)
);
let s = String::from_utf8_lossy(&out.stdout);
assert!(s.contains("verify OK"), "expected verify OK, got:\n{s}");
assert!(s.contains(".claude-plugin/marketplace.json validates"));
assert!(s.contains("skills/skillpack/SKILL.md validates"));
assert!(s.contains(".codex/skills/skillpack/SKILL.md validates"));
assert!(s.contains(".cursor/rules/skillpack.mdc validates"));
}