use std::path::{Path, PathBuf};
use std::process::{Command, Output};
fn bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_supercode"))
}
fn fixtures() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../harness/tests/fixtures")
}
fn scratch(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"supercode-orch11-{tag}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn write_skill(root: &Path, name: &str, body: &str) {
let dir = root.join(name);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("SKILL.md"), body).unwrap();
}
fn run(cwd: &Path, envs: &[(&str, PathBuf)], args: &[&str]) -> Output {
let home = scratch("home");
let mut command = Command::new(bin());
command
.current_dir(cwd)
.env("SUPERCODE_HOME", home.join("sessions"))
.env("HOME", &home)
.env("CLAUDE_CONFIG_DIR", home.join("absent-claude"))
.env("CODEX_HOME", home.join("absent-codex"))
.env("OPENCODE_CONFIG_DIR", home.join("absent-opencode"))
.env("PI_CODING_AGENT_DIR", home.join("absent-pi"))
.env("HERMES_HOME", home.join("absent-hermes"))
.env("OPENCLAW_STATE_DIR", home.join("absent-openclaw"))
.env("NO_COLOR", "1")
.env_remove("OPENROUTER_API_KEY");
for (key, value) in envs {
command.env(key, value);
}
let output = command
.args(args)
.stdin(std::process::Stdio::null())
.output()
.expect("supercode binary runs");
let _ = std::fs::remove_dir_all(&home);
output
}
fn stdout(cwd: &Path, envs: &[(&str, PathBuf)], args: &[&str]) -> String {
let output = run(cwd, envs, args);
assert!(
output.status.success(),
"`supercode {}` failed: {}",
args.join(" "),
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout).unwrap()
}
fn rows(cwd: &Path, envs: &[(&str, PathBuf)], args: &[&str]) -> Vec<serde_json::Value> {
serde_json::from_str(&stdout(cwd, envs, args)).expect("skills list --json is a JSON array")
}
#[test]
fn skills_list_json_reads_the_hermes_and_openclaw_fixture_homes() {
let fixtures = fixtures();
let cwd = scratch("cwd");
let rows = rows(
&cwd,
&[
("HERMES_HOME", fixtures.join("hermes_home")),
("OPENCLAW_STATE_DIR", fixtures.join("openclaw_home")),
],
&["skills", "list", "--json"],
);
let arxiv = rows
.iter()
.find(|row| row["name"] == "arxiv-search")
.unwrap_or_else(|| panic!("no arxiv-search row in {rows:#?}"));
assert_eq!(arxiv["harness"], "hermes");
assert_eq!(arxiv["scope"], "user");
assert_eq!(arxiv["version"], "1.4.0");
assert!(arxiv["description"]
.as_str()
.unwrap()
.contains("Search arXiv"));
let bare = rows
.iter()
.find(|row| row["name"] == "bare-skill")
.unwrap_or_else(|| panic!("no bare-skill row in {rows:#?}"));
assert_eq!(bare["harness"], "hermes");
assert_eq!(bare["enabled"], serde_json::Value::Null);
let demo = rows
.iter()
.find(|row| row["name"] == "clawhub-demo")
.unwrap_or_else(|| panic!("no clawhub-demo row in {rows:#?}"));
assert_eq!(demo["harness"], "openclaw");
assert_eq!(demo["scope"], "managed");
assert_eq!(demo["enabled"], false);
let _ = std::fs::remove_dir_all(&cwd);
}
#[test]
fn skills_list_table_names_harness_name_scope_and_location() {
let fixtures = fixtures();
let cwd = scratch("cwd");
let table = stdout(
&cwd,
&[("HERMES_HOME", fixtures.join("hermes_home"))],
&["skills", "list", "--harness", "hermes"],
);
let row = table
.lines()
.find(|line| line.contains("arxiv-search"))
.unwrap_or_else(|| panic!("no arxiv-search row in:\n{table}"));
assert!(row.contains("hermes"), "{row}");
assert!(row.contains("user"), "{row}");
assert!(row.contains("hermes_home/skills/research/arxiv"), "{row}");
let _ = std::fs::remove_dir_all(&cwd);
}
#[test]
fn skills_list_reads_a_claude_code_temp_home_and_project_root() {
let claude_home = scratch("claude");
write_skill(
&claude_home.join("skills"),
"note-taker",
"---\nname: note-taker\ndescription: Take notes.\nversion: 2.0.0\n---\n\nbody\n",
);
let project = scratch("project");
std::fs::create_dir_all(project.join(".git")).unwrap();
write_skill(
&project.join(".claude").join("skills"),
"repo-runbook",
"---\nname: repo-runbook\ndescription: Runbook for this repo.\n---\n\nbody\n",
);
let listed = rows(
&project,
&[("CLAUDE_CONFIG_DIR", claude_home.clone())],
&["skills", "list", "--harness", "claude-code", "--json"],
);
let personal = listed
.iter()
.find(|row| row["name"] == "note-taker")
.unwrap_or_else(|| panic!("no note-taker row in {listed:#?}"));
assert_eq!(personal["harness"], "claude-code");
assert_eq!(personal["scope"], "user");
assert_eq!(personal["version"], "2.0.0");
let repo = listed
.iter()
.find(|row| row["name"] == "repo-runbook")
.unwrap_or_else(|| panic!("no repo-runbook row in {listed:#?}"));
assert_eq!(repo["scope"], "project");
assert_eq!(repo["version"], serde_json::Value::Null);
let project_only = rows(
&project,
&[("CLAUDE_CONFIG_DIR", claude_home.clone())],
&[
"skills",
"list",
"--harness",
"claude-code",
"--scope",
"project",
"--json",
],
);
assert!(
project_only.iter().all(|row| row["scope"] == "project"),
"{project_only:#?}"
);
assert!(project_only.iter().any(|row| row["name"] == "repo-runbook"));
let _ = std::fs::remove_dir_all(&claude_home);
let _ = std::fs::remove_dir_all(&project);
}
#[test]
fn skills_list_refuses_an_unknown_harness() {
let cwd = scratch("cwd");
let output = run(&cwd, &[], &["skills", "list", "--harness", "notaharness"]);
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("notaharness"), "{stderr}");
assert!(stderr.contains("unsupported action"), "{stderr}");
let _ = std::fs::remove_dir_all(&cwd);
}