#![cfg(unix)]
use std::io::Write as _;
use std::os::unix::fs::PermissionsExt as _;
use std::path::PathBuf;
use std::process::{Command, Output};
use std::sync::atomic::{AtomicU64, Ordering};
use serde_json::Value;
fn skilltest() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_skilltest"))
}
fn fake_provider() -> PathBuf {
skilltest()
.parent()
.expect("binary has a parent dir")
.join("skilltest-fake-provider")
}
fn fixtures() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures")
}
fn temp_dir(tag: &str) -> PathBuf {
static N: AtomicU64 = AtomicU64::new(0);
let dir = std::env::temp_dir().join(format!(
"skilltest-clierr-{}-{tag}-{}",
std::process::id(),
N.fetch_add(1, Ordering::Relaxed)
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn script(dir: &std::path::Path, name: &str, body: &str) -> PathBuf {
let path = dir.join(name);
let mut f = std::fs::File::create(&path).unwrap();
f.write_all(format!("#!/bin/sh\n{body}").as_bytes())
.unwrap();
let mut perms = std::fs::metadata(&path).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&path, perms).unwrap();
path
}
fn fake_oneharness(dir: &std::path::Path, failure_kind: &str) -> PathBuf {
script(
dir,
"oneharness",
&format!(
"cat >/dev/null\necho '{{\"results\":[{{\"status\":\"error\",\
\"failure_kind\":\"{failure_kind}\",\"error\":\"simulated {failure_kind}\"}}]}}'\n"
),
)
}
fn run_passing_case(extra: &[&str]) -> Output {
let mut cmd = Command::new(skilltest());
cmd.arg("run")
.arg(fixtures().join("cases/greet_pass.yaml"))
.arg("--provider")
.arg(fake_provider())
.args(["--platform", "demo", "--model", "fake"])
.args(extra);
cmd.output().expect("skilltest run executes")
}
#[test]
fn classified_provider_errors_print_their_hint() {
let cases = [
("auth", "credentials"),
("rate_limit", "rate-limited"),
("model_not_found", "does not recognize this model"),
("quota", "quota"),
("overloaded", "overloaded"),
("some_other_kind", "see provider docs"),
];
for (kind, needle) in cases {
let dir = temp_dir(kind);
let oh = fake_oneharness(&dir, kind);
let out = Command::new(skilltest())
.arg("run")
.arg(fixtures().join("cases/greet_pass.yaml"))
.args(["--oneharness-bin", oh.to_str().unwrap()])
.args(["--platform", "claude-code", "--model", "sonnet"])
.output()
.expect("executes");
assert_eq!(
out.status.code(),
Some(3),
"{kind} should exit 3, stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains(needle),
"{kind} hint should mention {needle:?}, got: {stderr}"
);
}
}
#[test]
fn unclassified_provider_error_prints_generic_hint() {
let out = Command::new(skilltest())
.arg("run")
.arg(fixtures().join("cases/greet_pass.yaml"))
.args(["--provider", "/nonexistent/skilltest-provider"])
.args(["--platform", "demo", "--model", "fake"])
.output()
.expect("executes");
assert_eq!(out.status.code(), Some(3));
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("on PATH"), "generic hint: {stderr}");
}
#[test]
fn explicit_config_flag_is_loaded() {
let dir = temp_dir("config");
let cfg = dir.join("custom.yaml");
std::fs::write(
&cfg,
format!(
"provider:\n kind: command\n command: [\"{}\"]\nplatforms: [demo]\nmodels: [fake]\n",
fake_provider().display()
),
)
.unwrap();
let out = Command::new(skilltest())
.args(["--config", cfg.to_str().unwrap()])
.arg("run")
.arg(fixtures().join("cases/greet_pass.yaml"))
.args(["--format", "json"])
.output()
.expect("executes");
assert!(
out.status.success(),
"explicit config run passes: {}",
String::from_utf8_lossy(&out.stderr)
);
let report: Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(report["passed"], Value::Bool(true));
}
#[test]
fn explicit_config_missing_file_exits_two() {
let out = Command::new(skilltest())
.args(["--config", "/no/such/skilltest-config.yaml"])
.arg("run")
.arg(fixtures().join("cases/greet_pass.yaml"))
.output()
.expect("executes");
assert_eq!(
out.status.code(),
Some(2),
"missing config is a usage error"
);
}
#[test]
fn run_human_format_prints_a_summary() {
let out = run_passing_case(&[]);
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("PASS"), "human summary: {stdout}");
assert!(stdout.contains("runs passed"), "tally: {stdout}");
}
#[test]
fn validate_json_format_reports_findings() {
let out = Command::new(skilltest())
.arg("validate")
.arg(fixtures().join("skills/invalid"))
.args(["--format", "json"])
.output()
.expect("executes");
assert_eq!(out.status.code(), Some(1));
let report: Value = serde_json::from_slice(&out.stdout).expect("valid JSON");
assert_eq!(report["valid"], Value::Bool(false));
assert!(!report["findings"].as_array().unwrap().is_empty());
}
#[test]
fn validate_json_format_on_a_good_skill_is_valid() {
let out = Command::new(skilltest())
.arg("validate")
.arg(fixtures().join("skills/greeter"))
.args(["--format", "json"])
.output()
.expect("executes");
assert!(out.status.success());
let report: Value = serde_json::from_slice(&out.stdout).expect("valid JSON");
assert_eq!(report["valid"], Value::Bool(true));
}
#[test]
fn init_into_a_path_under_a_file_is_an_io_error() {
let dir = temp_dir("init-io");
let blocker = dir.join("blocker");
std::fs::write(&blocker, "i am a file, not a dir").unwrap();
let target = blocker.join("nested"); let out = Command::new(skilltest())
.arg("init")
.arg(&target)
.output()
.expect("executes");
assert_eq!(out.status.code(), Some(2), "io failure is a usage error");
}
#[test]
fn api_judge_without_key_is_a_classified_auth_error() {
let dir = temp_dir("apijudge");
let cfg = dir.join("skilltest.yaml");
std::fs::write(
&cfg,
format!(
"provider:\n kind: command\n command: [\"{}\"]\n\
platforms: [demo]\nmodels: [fake]\n\
judge:\n kind: api\n vendor: anthropic\n api_key_env: SKILLTEST_DEFINITELY_UNSET\n",
fake_provider().display()
),
)
.unwrap();
let out = Command::new(skilltest())
.args(["--config", cfg.to_str().unwrap()])
.arg("run")
.arg(fixtures().join("cases/greet_pass.yaml"))
.env_remove("SKILLTEST_DEFINITELY_UNSET")
.output()
.expect("executes");
assert_eq!(
out.status.code(),
Some(3),
"missing API key is a provider error, stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("credentials"), "auth hint: {stderr}");
}