use std::path::{Path, PathBuf};
use std::process::Command;
use serde_json::Value;
use tempfile::TempDir;
const SKIP_MARKER: &str = "# skill-example-ci: skip";
#[derive(Debug)]
struct Invocation {
skill: String,
line: usize,
argv: Vec<String>,
}
#[test]
fn every_skill_orchestratectl_example_matches_the_binary() {
let skills_dir = skills_dir();
let home = TempDir::new().expect("temp ORCHESTRATECTL_HOME");
let mut invocations = Vec::new();
for skill_md in skill_template_paths(&skills_dir) {
let skill_name = skill_md
.parent()
.and_then(|p| p.file_name())
.and_then(|s| s.to_str())
.unwrap_or("<unknown>")
.to_string();
let body = std::fs::read_to_string(&skill_md)
.unwrap_or_else(|e| panic!("read {}: {e}", skill_md.display()));
invocations.extend(extract_invocations(&skill_name, &body));
}
assert!(
!invocations.is_empty(),
"extracted zero orchestratectl invocations — the extractor or the \
skills directory ({}) is broken; this gate would be silently \
vacuous",
skills_dir.display()
);
let mut failures = Vec::new();
for inv in &invocations {
if let Err(reason) = validate(&inv.argv, &home) {
failures.push(format!(
" {} (line {}):\n $ orchestratectl {}\n → {}",
inv.skill,
inv.line,
inv.argv.join(" "),
reason
));
}
}
assert!(
failures.is_empty(),
"{} of {} documented orchestratectl invocation(s) do not match the \
binary's CLI surface.\n\nEach failing example below documents a flag, \
subcommand, positional, or enum value the binary does not accept. Fix \
the SKILL to match the binary (run the command with `--help` to see \
the real surface), or — if the example is genuinely illustrative and \
cannot parse — add a `{SKIP_MARKER}` comment above it.\n\n{}",
failures.len(),
invocations.len(),
failures.join("\n")
);
}
#[test]
fn gate_rejects_a_bogus_flag() {
let home = TempDir::new().expect("temp ORCHESTRATECTL_HOME");
let argv = [
"run".to_string(),
"create".to_string(),
"--kind".to_string(),
"fan-out".to_string(),
"--definitely-not-a-real-flag".to_string(),
];
let result = validate(&argv, &home);
let err = result.expect_err("gate must reject an unknown flag");
assert!(
err.contains("unknown_subcommand_or_flag"),
"expected an unknown-flag shape error, got: {err}"
);
}
#[test]
fn validation_has_no_side_effects() {
let home = TempDir::new().expect("temp ORCHESTRATECTL_HOME");
let argv = [
"run".to_string(),
"create".to_string(),
"--kind".to_string(),
"fan-out".to_string(),
"--title".to_string(),
"t".to_string(),
"--task".to_string(),
"do the thing".to_string(),
];
validate(&argv, &home).expect("a well-formed run create must validate");
assert!(
!home.path().join("runs").exists(),
"validation created a runs/ directory — `--help` did not short-circuit \
before the handler; the gate is no longer side-effect-free"
);
}
fn validate(argv: &[String], home: &TempDir) -> Result<(), String> {
let out = Command::new(env!("CARGO_BIN_EXE_orchestratectl"))
.env("ORCHESTRATECTL_HOME", home.path())
.env("HOME", home.path())
.args(argv)
.arg("--help")
.output()
.expect("spawn orchestratectl");
if out.status.success() {
return Ok(());
}
let stderr = String::from_utf8_lossy(&out.stderr);
let detail = serde_json::from_str::<Value>(stderr.trim())
.ok()
.and_then(|v| {
let err = v.get("error")?;
let code = err.get("code").and_then(Value::as_str).unwrap_or("error");
let msg = err.get("message").and_then(Value::as_str).unwrap_or("");
Some(format!("{code}: {msg}"))
})
.unwrap_or_else(|| stderr.trim().to_string());
Err(detail)
}
fn skills_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("skills")
}
fn skill_template_paths(skills_dir: &Path) -> Vec<PathBuf> {
let mut paths: Vec<PathBuf> = std::fs::read_dir(skills_dir)
.unwrap_or_else(|e| panic!("read {}: {e}", skills_dir.display()))
.flatten()
.map(|e| e.path().join("SKILL.template.md"))
.filter(|p| p.is_file())
.collect();
paths.sort();
paths
}
fn extract_invocations(skill: &str, body: &str) -> Vec<Invocation> {
let lines: Vec<&str> = body.lines().collect();
let mut out = Vec::new();
let mut in_fence = false;
let mut prev_nonblank: &str = "";
let mut i = 0;
while i < lines.len() {
let line = lines[i];
if line.trim_start().starts_with("```") {
in_fence = !in_fence;
prev_nonblank = "";
i += 1;
continue;
}
if in_fence && line.trim_start().starts_with("orchestratectl ") {
let first_line_no = i + 1; let mut joined = String::new();
loop {
let cur = lines[i];
let trimmed_end = cur.trim_end();
if let Some(stripped) = trimmed_end.strip_suffix('\\') {
joined.push_str(stripped.trim_end());
joined.push(' ');
i += 1;
if i >= lines.len() {
break;
}
} else {
joined.push_str(trimmed_end);
i += 1;
break;
}
}
let skip = joined.contains(SKIP_MARKER) || prev_nonblank.contains(SKIP_MARKER);
if !skip {
if let Some(argv) = to_argv(&joined) {
out.push(Invocation {
skill: skill.to_string(),
line: first_line_no,
argv,
});
}
}
prev_nonblank = "";
continue;
}
if in_fence && !line.trim().is_empty() {
prev_nonblank = line;
}
i += 1;
}
out
}
fn to_argv(command: &str) -> Option<Vec<String>> {
let mut tokens = shell_split(command).into_iter();
let head = tokens.next()?;
if head != "orchestratectl" {
return None;
}
let argv: Vec<String> = tokens
.map(|t| normalize_token(&t))
.filter(|t| !t.is_empty())
.collect();
Some(argv)
}
fn normalize_token(token: &str) -> String {
let unbracketed = token.trim_start_matches('[').trim_end_matches(']');
match unbracketed {
"<kind>" => "fan-out".to_string(),
other => other.replace("{{CLI_VERSION}}", env!("CARGO_PKG_VERSION")),
}
}
fn shell_split(s: &str) -> Vec<String> {
let mut tokens = Vec::new();
let mut cur = String::new();
let mut has_token = false;
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
match c {
c if c.is_whitespace() => {
if has_token {
tokens.push(std::mem::take(&mut cur));
has_token = false;
}
}
'#' if !has_token => break, '\'' => {
has_token = true;
for q in chars.by_ref() {
if q == '\'' {
break;
}
cur.push(q);
}
}
'"' => {
has_token = true;
while let Some(q) = chars.next() {
match q {
'"' => break,
'\\' => {
match chars.peek() {
Some('"' | '\\') => cur.push(chars.next().unwrap()),
_ => cur.push('\\'),
}
}
other => cur.push(other),
}
}
}
other => {
has_token = true;
cur.push(other);
}
}
}
if has_token {
tokens.push(cur);
}
tokens
}
#[cfg(test)]
mod extractor_tests {
use super::*;
#[test]
fn joins_continuations_and_strips_brackets() {
let body = "```\norchestratectl run create \\\n --kind fan-out \\\n [--source-branch <branch>]\n```\n";
let inv = extract_invocations("x", body);
assert_eq!(inv.len(), 1);
assert_eq!(
inv[0].argv,
vec![
"run",
"create",
"--kind",
"fan-out",
"--source-branch",
"<branch>"
]
);
}
#[test]
fn honours_skip_marker_above_and_inline() {
let above = "```\n# skill-example-ci: skip\norchestratectl frobnicate --wat\n```\n";
assert!(extract_invocations("x", above).is_empty());
let inline = "```\norchestratectl frobnicate --wat # skill-example-ci: skip\n```\n";
assert!(extract_invocations("x", inline).is_empty());
}
#[test]
fn ignores_orchestratectl_outside_fences() {
let prose = "Run `orchestratectl run list` to see runs.\n";
assert!(extract_invocations("x", prose).is_empty());
}
#[test]
fn substitutes_kind_and_version_placeholders() {
let body = "```\norchestratectl run create --kind <kind>\n```\n";
let inv = extract_invocations("x", body);
assert_eq!(inv[0].argv, vec!["run", "create", "--kind", "fan-out"]);
}
}