use std::path::Path;
use assert_cmd::Command;
use predicates::prelude::*;
fn rigger(data: &Path) -> Command {
let mut cmd = Command::cargo_bin("rigger").unwrap();
cmd.env("RIGGER_DATA_DIR", data);
cmd.env("RIGGER_SKILLS_DIR", data.join("skills"));
cmd
}
fn output(data: &Path, args: &[&str]) -> String {
let out = rigger(data).args(args).assert().success();
String::from_utf8(out.get_output().stdout.clone()).unwrap()
}
fn line(data: &Path) {
rigger(data).arg("init").assert().success();
for (name, about) in [("sample", Some("A sample product")), ("other", None)] {
let root = data.join(name);
std::fs::create_dir_all(&root).unwrap();
if let Some(about) = about {
std::fs::write(root.join("Cargo.toml"), format!("[package]\nname = \"{name}\"\ndescription = \"{about}\"\n")).unwrap();
}
rigger(data).args(["project", "add"]).arg(&root).assert().success();
}
}
#[test]
fn one_skill_names_every_project_and_tells_an_assistant_how_to_pick_one() {
let data = tempfile::tempdir().unwrap();
line(data.path());
let skill = output(data.path(), &["skill", "--line"]);
assert!(skill.starts_with("---\nname: line\n"), "{skill}");
assert!(skill.contains("<!-- generated by rigger skill"), "{skill}");
assert!(skill.contains("| `sample` | A sample product |"), "{skill}");
assert!(skill.contains("| `other` |"), "{skill}");
assert!(skill.contains("rigger context <project>"), "{skill}");
assert!(skill.contains("rigger session end <project>"), "{skill}");
}
#[test]
fn the_description_is_within_what_an_assistant_accepts() {
let data = tempfile::tempdir().unwrap();
line(data.path());
let skill = output(data.path(), &["skill", "--line"]);
let description = skill
.lines()
.find_map(|l| l.strip_prefix("description: "))
.expect("the skill declares a description");
assert!(
description.chars().count() <= 1024,
"the description is {} characters",
description.chars().count()
);
assert!(!description.contains('<') && !description.contains('>'), "{description}");
assert!(description.contains("sample"), "{description}");
}
#[test]
fn a_description_over_the_limit_is_refused_rather_than_written() {
let data = tempfile::tempdir().unwrap();
line(data.path());
let long = "x".repeat(1100);
std::fs::write(
data.path().join("skill.line.md"),
format!("---\nname: {{{{line}}}}\ndescription: {long}\n---\n\n{{{{projects}}}}\n"),
)
.unwrap();
let skill = output(data.path(), &["skill", "--line"]);
assert!(skill.contains(&long), "a template's own text is its own business: {}", skill.len());
let composed = output(data.path(), &["skill", "--line"]);
assert!(!composed.lines().any(|l| l.starts_with("description: ") && (l.contains('<') || l.contains('>'))));
}
#[test]
fn a_project_added_later_reaches_the_skill_that_is_already_installed() {
let data = tempfile::tempdir().unwrap();
line(data.path());
rigger(data.path()).args(["skill", "--line", "--install"]).assert().success();
let path = data.path().join("skills").join("line").join("SKILL.md");
let before = std::fs::read_to_string(&path).unwrap();
assert!(!before.contains("`third`"), "{before}");
let root = data.path().join("third");
std::fs::create_dir_all(&root).unwrap();
rigger(data.path()).args(["project", "add"]).arg(&root).assert().success();
let after = std::fs::read_to_string(&path).unwrap();
assert!(
after.contains("| `third` |"),
"a skill that has never heard of the new project is the fault this prevents:\n{after}"
);
}
#[test]
fn project_add_does_not_install_a_skill_nobody_asked_for() {
let data = tempfile::tempdir().unwrap();
line(data.path());
let root = data.path().join("third");
std::fs::create_dir_all(&root).unwrap();
rigger(data.path()).args(["project", "add"]).arg(&root).assert().success();
assert!(!data.path().join("skills").join("line").join("SKILL.md").exists());
}
#[test]
fn a_run_with_a_record_of_its_own_does_not_reach_into_the_home_directory() {
let data = tempfile::tempdir().unwrap();
let home = tempfile::tempdir().unwrap();
let mut init = Command::cargo_bin("rigger").unwrap();
init.env("RIGGER_DATA_DIR", data.path()).env_remove("RIGGER_SKILLS_DIR");
init.arg("init").assert().success();
let skills = home.path().join(".claude").join("skills").join("line");
std::fs::create_dir_all(&skills).unwrap();
let path = skills.join("SKILL.md");
let before = format!(
"---
name: line
description: the owner's own
---
{}
# line
",
"<!-- generated by rigger skill; edit the template, not this file -->"
);
std::fs::write(&path, &before).unwrap();
let root = data.path().join("sample");
std::fs::create_dir_all(&root).unwrap();
let mut add = Command::cargo_bin("rigger").unwrap();
add.env("RIGGER_DATA_DIR", data.path())
.env_remove("RIGGER_SKILLS_DIR")
.env("HOME", home.path())
.env("USERPROFILE", home.path());
add.args(["project", "add"]).arg(&root).assert().success();
assert_eq!(
std::fs::read_to_string(&path).unwrap(),
before,
"a run with its own record rewrote a skill in the home directory"
);
}
#[test]
fn the_registry_publishes_what_is_public_and_nothing_else() {
let data = tempfile::tempdir().unwrap();
line(data.path());
rigger(data.path())
.args([
"project",
"mark",
"sample",
"--code",
"sa",
"--accent",
"#3FA873",
"--form",
"cli",
"--docs",
"https://example.github.io/sample/",
])
.assert()
.success();
let json = output(data.path(), &["export", "--line"]);
let registry: serde_json::Value = serde_json::from_str(&json).expect("the registry is JSON");
let products = registry["products"].as_array().unwrap();
assert_eq!(products.len(), 2, "{json}");
let sample = products.iter().find(|p| p["name"] == "sample").unwrap();
assert_eq!(sample["mark"], "sa");
assert_eq!(sample["accent"], "#3FA873");
assert_eq!(sample["form"], "cli");
assert_eq!(sample["docs"], "https://example.github.io/sample/");
let other = products.iter().find(|p| p["name"] == "other").unwrap();
assert!(other["mark"].is_null(), "an undrawn mark is absent, not invented: {json}");
assert!(!json.contains(&data.path().display().to_string()), "{json}");
}
#[test]
fn a_registry_carrying_a_private_path_is_refused_before_it_is_written() {
let data = tempfile::tempdir().unwrap();
line(data.path());
std::fs::write(
data.path().join("sample").join("Cargo.toml"),
"[package]\nname = \"sample\"\ndescription = \"the one in C:\\\\Projects\\\\sample\"\n",
)
.unwrap();
let target = data.path().join("line.json");
rigger(data.path())
.args(["export", "--line", "--to"])
.arg(&target)
.assert()
.failure()
.stderr(predicate::str::contains("path on a machine"));
assert!(!target.exists(), "nothing is written when the check refuses");
}
#[test]
fn a_mark_is_checked_rather_than_taken_at_its_word() {
let data = tempfile::tempdir().unwrap();
line(data.path());
for (args, expected) in [
(["--code", "SA"], "two lowercase letters"),
(["--accent", "3FA873"], "#RRGGBB"),
(["--form", "app"], "desktop"),
] {
rigger(data.path())
.args(["project", "mark", "sample"])
.args(args)
.assert()
.failure()
.stderr(predicate::str::contains(expected));
}
rigger(data.path()).args(["project", "mark", "sample", "--code", "sa"]).assert().success();
rigger(data.path()).args(["project", "mark", "other", "--code", "sa"]).assert().failure();
}
#[test]
fn stating_one_field_of_a_mark_leaves_the_others_alone() {
let data = tempfile::tempdir().unwrap();
line(data.path());
rigger(data.path())
.args(["project", "mark", "sample", "--code", "sa", "--accent", "#3FA873"])
.assert()
.success();
rigger(data.path()).args(["project", "mark", "sample", "--form", "cli"]).assert().success();
let shown = output(data.path(), &["project", "mark", "sample"]);
assert!(shown.contains("sa") && shown.contains("#3FA873") && shown.contains("cli"), "{shown}");
rigger(data.path()).args(["project", "mark", "sample", "--clear"]).assert().success();
let cleared = output(data.path(), &["project", "mark", "sample"]);
assert!(cleared.contains("no mark recorded"), "{cleared}");
}
#[test]
fn a_search_reaches_the_documents_and_says_how_to_open_one() {
let data = tempfile::tempdir().unwrap();
line(data.path());
rigger(data.path())
.args(["doc", "add", "sample", "Vision of sample", "--kind", "vision", "--body"])
.arg("The product exists to keep one seat for every project.")
.assert()
.success();
let found = output(data.path(), &["find", "seat"]);
assert!(found.contains("Documents"), "documents are named as such: {found}");
assert!(found.contains("rigger doc show sample"), "and carry the command that opens one: {found}");
let by_kind = output(data.path(), &["find", "seat", "--kind", "decision"]);
assert!(!by_kind.contains("Documents"), "{by_kind}");
}
#[test]
fn a_document_is_searchable_as_soon_as_it_changes() {
let data = tempfile::tempdir().unwrap();
line(data.path());
rigger(data.path())
.args(["doc", "add", "sample", "Vision", "--kind", "vision", "--body"])
.arg("The first words.")
.assert()
.success();
assert!(output(data.path(), &["find", "first"]).contains("Documents"));
rigger(data.path())
.args(["doc", "edit", "sample", "vision", "--body"])
.arg("Entirely different words.")
.assert()
.success();
let after = output(data.path(), &["find", "different"]);
assert!(after.contains("Documents"), "an edited document is searchable by its new words: {after}");
let stale = output(data.path(), &["find", "first"]);
assert!(!stale.contains("Documents"), "and no longer by its old ones: {stale}");
}
#[test]
fn the_screen_says_what_the_project_is_and_what_it_has_written() {
let data = tempfile::tempdir().unwrap();
line(data.path());
rigger(data.path())
.args(["doc", "add", "sample", "Vision of sample", "--kind", "vision", "--body"])
.arg("Why it exists.")
.assert()
.success();
rigger(data.path())
.args(["project", "set", "sample", "--gate", "cargo test"])
.assert()
.success();
let screen = output(data.path(), &["show", "sample"]);
assert!(screen.starts_with("sample\nA sample product\n"), "{screen}");
assert!(screen.contains("cargo test"), "{screen}");
assert!(screen.contains("Nothing shipped yet"), "{screen}");
assert!(screen.contains("rigger doc show sample vision"), "{screen}");
assert!(!screen.contains("left out by the budget"), "{screen}");
}
#[test]
fn a_closing_sitting_runs_what_the_project_asks_and_records_how_it_went() {
let data = tempfile::tempdir().unwrap();
line(data.path());
rigger(data.path())
.args(["project", "set", "sample", "--on-session-end", "cd ."])
.assert()
.success();
rigger(data.path()).args(["session", "start", "sample"]).assert().success();
let ended = output(data.path(), &["session", "end", "sample"]);
assert!(ended.contains("cd ."), "the command is named in what the close reports: {ended}");
assert!(ended.contains("green"), "{ended}");
let found = output(data.path(), &["find", "end of session"]);
assert!(found.contains("change"), "{found}");
}
#[test]
fn a_red_command_is_reported_but_does_not_stop_the_session_closing() {
let data = tempfile::tempdir().unwrap();
line(data.path());
rigger(data.path())
.args(["project", "set", "sample", "--on-session-end", "exit 3"])
.assert()
.success();
rigger(data.path()).args(["session", "start", "sample"]).assert().success();
let ended = output(data.path(), &["session", "end", "sample"]);
assert!(ended.contains("red (exit 3)"), "{ended}");
assert!(ended.contains("The ritual asks for:"), "{ended}");
assert!(ended.contains("came back red (exit 3)"), "it is listed among what is missing: {ended}");
let again = output(data.path(), &["session", "end", "sample"]);
assert!(again.contains("No session is open"), "{again}");
}
#[test]
fn the_packet_names_the_documents_without_carrying_them() {
let data = tempfile::tempdir().unwrap();
line(data.path());
let long = "A vision that goes on. ".repeat(200);
rigger(data.path())
.args(["doc", "add", "sample", "Vision of sample", "--kind", "vision", "--body"])
.arg(&long)
.assert()
.success();
let packet = output(data.path(), &["context", "sample"]);
assert!(packet.contains("## Written down"), "{packet}");
assert!(packet.contains("Vision of sample"), "{packet}");
assert!(packet.contains("rigger doc show sample vision"), "{packet}");
assert!(!packet.contains("A vision that goes on. A vision"), "{packet}");
}
#[test]
fn the_doctor_says_whether_the_other_door_opens() {
let data = tempfile::tempdir().unwrap();
line(data.path());
let report = output(data.path(), &["doctor"]);
assert!(report.contains("mcp:"), "{report}");
assert!(report.contains("answers"), "{report}");
assert!(report.contains("tools"), "{report}");
}