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
}
fn repo(root: &Path) {
std::fs::create_dir_all(root).unwrap();
std::fs::write(root.join("Cargo.toml"), "[package]\nname = \"declared-elsewhere\"\nversion = \"0.1.0\"\n").unwrap();
}
#[test]
fn version_prints_the_crate_version() {
Command::cargo_bin("rigger")
.unwrap()
.arg("--version")
.assert()
.success()
.stdout(predicate::str::contains(env!("CARGO_PKG_VERSION")));
}
#[test]
fn help_describes_the_product() {
Command::cargo_bin("rigger")
.unwrap()
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("One seat for all your projects and tasks"));
}
#[test]
fn init_creates_the_database_and_is_idempotent() {
let data = tempfile::tempdir().unwrap();
rigger(data.path())
.arg("init")
.assert()
.success()
.stdout(predicate::str::contains("Created").and(predicate::str::contains("schema version")));
assert!(data.path().join("profiles").join("line").join("rigger.db").exists());
assert!(data.path().join("config.toml").exists(), "init writes the config with the default profile");
rigger(data.path())
.arg("init")
.assert()
.success()
.stdout(predicate::str::contains("Already initialised"));
}
#[test]
fn commands_before_init_point_at_init() {
let data = tempfile::tempdir().unwrap();
rigger(data.path())
.args(["project", "list"])
.assert()
.failure()
.stderr(predicate::str::contains("run `rigger init` first"));
}
#[test]
fn project_add_names_the_project_after_its_directory() {
let data = tempfile::tempdir().unwrap();
let root = data.path().join("repos").join("folder-name");
repo(&root);
rigger(data.path()).arg("init").assert().success();
rigger(data.path())
.args(["project", "add"])
.arg(&root)
.assert()
.success()
.stdout(predicate::str::contains("Recorded 'folder-name'").and(predicate::str::contains("remote: none")));
rigger(data.path())
.args(["project", "list"])
.assert()
.success()
.stdout(predicate::str::starts_with("folder-name"));
}
#[test]
fn project_add_reads_the_origin_remote() {
let data = tempfile::tempdir().unwrap();
let root = data.path().join("remoted");
repo(&root);
std::fs::create_dir(root.join(".git")).unwrap();
std::fs::write(
root.join(".git").join("config"),
"[remote \"origin\"]\n\turl = https://example.com/remoted.git\n",
)
.unwrap();
rigger(data.path()).arg("init").assert().success();
rigger(data.path())
.args(["project", "add"])
.arg(&root)
.assert()
.success()
.stdout(predicate::str::contains("remote: https://example.com/remoted.git"));
rigger(data.path())
.args(["project", "show", "remoted", "--json"])
.assert()
.success()
.stdout(predicate::str::contains("\"remote\": \"https://example.com/remoted.git\""));
}
#[test]
fn name_can_be_overridden_and_must_be_unique() {
let data = tempfile::tempdir().unwrap();
let first = data.path().join("a").join("same");
let second = data.path().join("b").join("same");
repo(&first);
repo(&second);
rigger(data.path()).arg("init").assert().success();
rigger(data.path()).args(["project", "add"]).arg(&first).assert().success();
rigger(data.path())
.args(["project", "add"])
.arg(&second)
.assert()
.failure()
.stderr(predicate::str::contains("a project named 'same' already exists"));
rigger(data.path())
.args(["project", "add", "--name", "other"])
.arg(&second)
.assert()
.success()
.stdout(predicate::str::contains("Recorded 'other'"));
}
#[test]
fn the_same_path_is_not_recorded_twice() {
let data = tempfile::tempdir().unwrap();
let root = data.path().join("once");
repo(&root);
rigger(data.path()).arg("init").assert().success();
rigger(data.path()).args(["project", "add"]).arg(&root).assert().success();
rigger(data.path())
.args(["project", "add"])
.arg(&root)
.assert()
.failure()
.stderr(predicate::str::contains("already recorded as project 'once'"));
}
#[test]
fn show_of_an_unknown_project_fails_with_a_pointer() {
let data = tempfile::tempdir().unwrap();
rigger(data.path()).arg("init").assert().success();
rigger(data.path())
.args(["project", "show", "ghost"])
.assert()
.failure()
.stderr(predicate::str::contains("no project named 'ghost'"));
}
fn hub(dir: &Path) {
std::fs::create_dir_all(dir).unwrap();
std::fs::write(
dir.join("План.md"),
"# План разработки\n\n## Ждёт решения владельца\n\n1. Pick a colour.\n2. Sign the binary.\n\n## v0.3.0 · Third stage\n\n- [ ] first task\n- [ ] second task\n\n## Бэклог без версии\n\n- [ ] someday, not a task of a stage\n",
)
.unwrap();
std::fs::write(
dir.join("Изменения.md"),
"# Изменения\n\n## v0.2.0 · Second stage — выпущена 2026-09-03\n\n- Something shipped.\n\n## v0.1.0 · First stage — выпущен 2026-09-01\n",
)
.unwrap();
std::fs::write(
dir.join("Решения.md"),
"# Журнал решений\n\n---\n\n## 2026-09-03 · The record is the database\n\nBecause prose cannot be filtered.\n",
)
.unwrap();
}
fn imported_project(data: &Path) -> std::path::PathBuf {
let root = data.join("proj");
repo(&root);
hub(&root.join("hub"));
rigger(data).arg("init").assert().success();
rigger(data).args(["project", "add"]).arg(&root).assert().success();
root
}
#[test]
fn import_reads_stages_tasks_decisions_and_questions() {
let data = tempfile::tempdir().unwrap();
let root = imported_project(data.path());
rigger(data.path())
.args(["import", "proj", "--hub"])
.arg(root.join("hub"))
.assert()
.success()
.stdout(
predicate::str::contains("versions 3 added")
.and(predicate::str::contains("tasks 2 added"))
.and(predicate::str::contains("decisions 1 added"))
.and(predicate::str::contains("questions 2 added")),
);
rigger(data.path()).arg("doctor").assert().success().stdout(
predicate::str::contains("versions: 3")
.and(predicate::str::contains("tasks: 2"))
.and(predicate::str::contains("events: 3")),
);
}
#[test]
fn importing_an_unchanged_hub_again_changes_nothing() {
let data = tempfile::tempdir().unwrap();
let root = imported_project(data.path());
let hub_dir = root.join("hub");
rigger(data.path()).args(["import", "proj", "--hub"]).arg(&hub_dir).assert().success();
rigger(data.path())
.args(["import", "proj", "--hub"])
.arg(&hub_dir)
.assert()
.success()
.stdout(predicate::str::contains("nothing changed"));
rigger(data.path())
.arg("doctor")
.assert()
.success()
.stdout(predicate::str::contains("versions: 3").and(predicate::str::contains("events: 3")));
}
#[test]
fn a_stage_that_shipped_is_updated_rather_than_duplicated() {
let data = tempfile::tempdir().unwrap();
let root = imported_project(data.path());
let hub_dir = root.join("hub");
rigger(data.path()).args(["import", "proj", "--hub"]).arg(&hub_dir).assert().success();
std::fs::write(hub_dir.join("План.md"), "# План\n\n## Ждёт решения владельца\n\n- (пусто)\n").unwrap();
std::fs::write(
hub_dir.join("Изменения.md"),
"# Изменения\n\n## v0.3.0 · Third stage — выпущена 2026-09-05\n\n## v0.2.0 · Second stage — выпущена 2026-09-03\n\n## v0.1.0 · First stage — выпущен 2026-09-01\n",
)
.unwrap();
rigger(data.path())
.args(["import", "proj", "--hub"])
.arg(&hub_dir)
.assert()
.success()
.stdout(predicate::str::contains("versions 0 added"));
rigger(data.path())
.args(["import", "proj", "--hub"])
.arg(&hub_dir)
.assert()
.success()
.stdout(predicate::str::contains("nothing changed"));
rigger(data.path())
.arg("doctor")
.assert()
.success()
.stdout(predicate::str::contains("versions: 3"));
}
#[test]
fn a_stage_renumbered_by_its_release_replaces_the_old_row() {
let data = tempfile::tempdir().unwrap();
let root = imported_project(data.path());
let hub_dir = root.join("hub");
std::fs::write(hub_dir.join("Изменения.md"), "# Изменения\n\n## v1.9 · Очередь — закрыт 2026-09-03\n").unwrap();
rigger(data.path()).args(["import", "proj", "--hub"]).arg(&hub_dir).assert().success();
std::fs::write(
hub_dir.join("Изменения.md"),
"# Изменения\n\n## v1.9 · Очередь — закрыт 2026-09-03, выпущен **v1.9.0**\n",
)
.unwrap();
rigger(data.path()).args(["import", "proj", "--hub"]).arg(&hub_dir).assert().success();
rigger(data.path())
.args(["context", "proj"])
.assert()
.success()
.stdout(predicate::str::contains("Last shipped: v1.9.0"));
rigger(data.path())
.arg("doctor")
.assert()
.success()
.stdout(predicate::str::contains("versions: 2"));
}
#[test]
fn the_newest_release_is_the_highest_number_not_the_last_row() {
let data = tempfile::tempdir().unwrap();
let root = imported_project(data.path());
let hub_dir = root.join("hub");
std::fs::write(
hub_dir.join("Изменения.md"),
"# Изменения\n\n## v0.10.0 · Tenth — выпущена 2026-09-02\n\n## v0.9.0 · Ninth — выпущена 2026-09-02\n\n## v0.1.0 · First — выпущена 2026-09-02\n",
)
.unwrap();
rigger(data.path()).args(["import", "proj", "--hub"]).arg(&hub_dir).assert().success();
rigger(data.path())
.args(["context", "proj"])
.assert()
.success()
.stdout(predicate::str::contains("Last shipped: v0.10.0"));
}
#[test]
fn import_of_a_missing_hub_is_refused() {
let data = tempfile::tempdir().unwrap();
let root = imported_project(data.path());
rigger(data.path())
.args(["import", "proj", "--hub"])
.arg(root.join("no-such-hub"))
.assert()
.failure()
.stderr(predicate::str::contains("is not there"));
}
#[test]
fn import_of_an_unknown_project_fails() {
let data = tempfile::tempdir().unwrap();
let root = imported_project(data.path());
rigger(data.path())
.args(["import", "ghost", "--hub"])
.arg(root.join("hub"))
.assert()
.failure()
.stderr(predicate::str::contains("no project named 'ghost'"));
}
#[test]
fn context_shows_where_the_project_stands_and_what_is_next() {
let data = tempfile::tempdir().unwrap();
let root = imported_project(data.path());
rigger(data.path()).args(["import", "proj", "--hub"]).arg(root.join("hub")).assert().success();
rigger(data.path()).args(["context", "proj"]).assert().success().stdout(
predicate::str::contains("Last shipped: v0.2.0 on 2026-09-03")
.and(predicate::str::contains("## Current stage: v0.3.0 · Third stage"))
.and(predicate::str::contains("- first task"))
.and(predicate::str::contains("## Waiting for the owner"))
.and(predicate::str::contains("Pick a colour."))
.and(predicate::str::contains("The record is the database")),
);
}
#[test]
fn a_recorded_note_reaches_the_packet_and_a_next_step_stands_alone() {
let data = tempfile::tempdir().unwrap();
imported_project(data.path());
rigger(data.path())
.args(["note", "proj", "The parser must take hubs as they are.", "--kind", "finding"])
.assert()
.success()
.stdout(predicate::str::contains("Recorded a finding for proj"));
rigger(data.path())
.args(["note", "proj", "Ship the importer next.", "--kind", "next"])
.assert()
.success();
rigger(data.path()).args(["wish", "proj", "Show days without a commit."]).assert().success();
rigger(data.path()).args(["context", "proj"]).assert().success().stdout(
predicate::str::contains("finding · The parser must take hubs as they are.")
.and(predicate::str::contains("## Wishes, not yet sorted"))
.and(predicate::str::contains("Show days without a commit."))
.and(predicate::str::contains("## Next step\nShip the importer next."))
.and(predicate::str::contains("next · Ship").not()),
);
}
fn packet_cost(data: &Path, args: &[&str]) -> (usize, String) {
let mut cmd = rigger(data);
cmd.args(["context", "proj", "--explain"]).args(args);
let text = String::from_utf8(cmd.output().unwrap().stdout).unwrap();
let total = text
.lines()
.find(|l| l.starts_with("total"))
.and_then(|l| l.split_whitespace().nth(1))
.and_then(|n| n.parse().ok())
.unwrap_or_else(|| panic!("no total line in:\n{text}"));
(total, text)
}
#[test]
fn the_packet_holds_the_budget_and_reports_what_it_dropped() {
let data = tempfile::tempdir().unwrap();
let root = imported_project(data.path());
rigger(data.path()).args(["import", "proj", "--hub"]).arg(root.join("hub")).assert().success();
let body = "**A decision that took a paragraph.**\n\n".to_string() + &"Reasoning that runs on and on. ".repeat(40);
for i in 0..60 {
rigger(data.path())
.args(["note", "proj", &format!("{body} number {i} of sixty"), "--kind", "decision"])
.assert()
.success();
}
let (total, text) = packet_cost(data.path(), &[]);
assert!(total <= 3000, "packet is {total} tokens, over the budget:\n{text}");
let (total, text) = packet_cost(data.path(), &["--budget", "600"]);
assert!(total <= 600, "packet is {total} tokens, over the 600 asked for:\n{text}");
assert!(
text.contains("older events left out by the budget"),
"the packet did not say what it dropped:\n{text}"
);
}
#[test]
fn the_essentials_survive_an_impossible_budget() {
let data = tempfile::tempdir().unwrap();
let root = imported_project(data.path());
rigger(data.path()).args(["import", "proj", "--hub"]).arg(root.join("hub")).assert().success();
let (_, text) = packet_cost(data.path(), &["--budget", "1"]);
assert!(text.contains("## Current stage: v0.3.0"), "{text}");
assert!(text.contains("## Waiting for the owner"), "{text}");
assert!(text.contains("left out by the budget"), "{text}");
}
#[test]
fn context_as_json_carries_the_same_facts() {
let data = tempfile::tempdir().unwrap();
let root = imported_project(data.path());
rigger(data.path()).args(["import", "proj", "--hub"]).arg(root.join("hub")).assert().success();
rigger(data.path()).args(["context", "proj", "--json"]).assert().success().stdout(
predicate::str::contains("\"project\": \"proj\"")
.and(predicate::str::contains("\"version\": \"v0.3.0\""))
.and(predicate::str::contains("\"events_omitted\": 0")),
);
}
#[test]
fn open_print_shows_the_message_the_assistant_would_receive() {
let data = tempfile::tempdir().unwrap();
let root = imported_project(data.path());
rigger(data.path()).args(["import", "proj", "--hub"]).arg(root.join("hub")).assert().success();
rigger(data.path()).args(["open", "proj", "--print"]).assert().success().stdout(
predicate::str::contains("This is where the project stands")
.and(predicate::str::contains("rigger note"))
.and(predicate::str::contains("## Current stage: v0.3.0 · Third stage")),
);
}
#[test]
fn open_runs_the_assistant_in_the_project_with_the_packet() {
let data = tempfile::tempdir().unwrap();
let root = imported_project(data.path());
let log = data.path().join("assistant.log");
let (stub, args) = stub_assistant(data.path(), &log, 0);
let mut cmd = rigger(data.path());
cmd.env(open_env(), format!("{stub} {args}"));
cmd.args(["open", "proj"]).assert().success();
let recorded = std::fs::read_to_string(&log).unwrap();
assert!(
recorded.contains("This is where the project stands"),
"the packet did not reach the assistant: {recorded}"
);
let cwd = recorded
.lines()
.find_map(|l| l.trim_start_matches('\u{feff}').strip_prefix("cwd="))
.unwrap_or_else(|| panic!("the assistant did not report where it ran: {recorded}"));
assert!(
cwd.to_lowercase().ends_with("proj"),
"the assistant did not run in the project: ran in {cwd}, expected {}",
root.display()
);
}
#[cfg(windows)]
#[test]
fn open_can_launch_a_batch_file_assistant() {
let data = tempfile::tempdir().unwrap();
imported_project(data.path());
let log = data.path().join("batch.log");
let script = data.path().join("assistant.cmd");
std::fs::write(
&script,
format!(
"@echo off\r\necho cwd=%CD%>\"{}\"\r\nfindstr /n \"^\">>\"{}\"\r\n",
log.display(),
log.display()
),
)
.unwrap();
let mut cmd = rigger(data.path());
cmd.env(open_env(), script.display().to_string());
cmd.args(["open", "proj"]).assert().success();
let recorded = std::fs::read_to_string(&log).unwrap();
assert!(
recorded.contains("This is where the project stands"),
"a .cmd assistant did not receive the packet: {recorded}"
);
}
#[cfg(windows)]
#[test]
fn a_batch_assistant_receives_every_line_of_the_packet() {
let data = tempfile::tempdir().unwrap();
let root = imported_project(data.path());
rigger(data.path()).args(["import", "proj", "--hub"]).arg(root.join("hub")).assert().success();
rigger(data.path())
.args(["note", "proj", "Runs `open <project>` & pipes a|b, then ^ escapes.", "--kind", "finding"])
.assert()
.success();
let log = data.path().join("meta.log");
let script = data.path().join("assistant.cmd");
std::fs::write(&script, format!("@echo off\r\nfindstr /n \"^\">\"{}\"\r\n", log.display())).unwrap();
let mut cmd = rigger(data.path());
cmd.env(open_env(), script.display().to_string());
cmd.args(["open", "proj"]).assert().success();
let recorded = std::fs::read_to_string(&log).unwrap();
assert!(
recorded.contains("`open <project>` & pipes a|b, then ^ escapes."),
"the packet did not arrive whole: {recorded}"
);
assert!(recorded.contains("## Current stage"), "the packet was cut short: {recorded}");
let strays: Vec<_> = std::fs::read_dir(data.path())
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|n| n.contains("project") || n.contains("pipes"))
.collect();
assert!(strays.is_empty(), "cmd redirected into {strays:?}");
}
#[test]
fn open_passes_on_the_assistants_exit_status() {
let data = tempfile::tempdir().unwrap();
imported_project(data.path());
let log = data.path().join("assistant.log");
let (stub, args) = stub_assistant(data.path(), &log, 3);
let mut cmd = rigger(data.path());
cmd.env(open_env(), format!("{stub} {args}"));
cmd.args(["open", "proj"]).assert().code(3);
}
#[test]
fn open_reports_a_missing_assistant_by_name() {
let data = tempfile::tempdir().unwrap();
imported_project(data.path());
let mut cmd = rigger(data.path());
cmd.env(open_env(), "no-such-assistant-binary");
cmd.args(["open", "proj"])
.assert()
.failure()
.stderr(predicate::str::contains("cannot run `no-such-assistant-binary`"));
}
fn open_env() -> &'static str {
"RIGGER_ASSISTANT"
}
fn stub_assistant(dir: &Path, log: &Path, code: i32) -> (String, String) {
let log = log.display().to_string().replace('\\', "/");
if cfg!(windows) {
let script = dir.join("stub.ps1");
std::fs::write(
&script,
format!("\"cwd=$((Get-Location).Path)\" | Out-File -Append -Encoding utf8 '{log}'\n$args | Out-File -Append -Encoding utf8 '{log}'\nexit {code}\n"),
)
.unwrap();
(
"powershell.exe".to_string(),
format!("-NoProfile -ExecutionPolicy Bypass -File {}", script.display()),
)
} else {
let script = dir.join("stub.sh");
std::fs::write(
&script,
format!("#!/bin/sh\necho \"cwd=$(pwd)\" >> '{log}'\necho \"$@\" >> '{log}'\nexit {code}\n"),
)
.unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
}
("sh".to_string(), script.display().to_string())
}
}
#[test]
fn backup_copies_the_database_beside_itself() {
let data = tempfile::tempdir().unwrap();
rigger(data.path()).arg("init").assert().success();
rigger(data.path())
.arg("backup")
.assert()
.success()
.stdout(predicate::str::contains("Copied to"));
let copies: Vec<_> = std::fs::read_dir(data.path().join("profiles").join("line"))
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().ends_with(".bak"))
.collect();
assert_eq!(copies.len(), 1, "expected one backup, found {copies:?}");
let name = copies[0].file_name().to_string_lossy().to_string();
assert!(
name.starts_with("rigger.v") && name.contains('-'),
"a backup must name the schema it holds: {name}"
);
}
fn copies(data: &Path) -> Vec<String> {
let mut found: Vec<String> = std::fs::read_dir(data.join("profiles").join("line"))
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().to_string())
.filter(|n| n.ends_with(".bak"))
.collect();
found.sort_by_key(|name| {
let stem = name.strip_suffix(".bak").unwrap_or(name);
let tail = stem.rsplit_once(".v").map(|(_, t)| t).unwrap_or("");
let mut parts = tail.split('-');
let moment = format!("{}{}", parts.nth(1).unwrap_or(""), parts.next().unwrap_or(""));
let seq: u32 = parts.next().map(|n| n.parse().unwrap_or(0)).unwrap_or(1);
(moment, seq, name.clone())
});
found.reverse();
found
}
fn copy_aged(data: &Path, days: i64) -> String {
let at = jiff::Timestamp::now().as_second() - days * 86_400;
let stamp = jiff::Timestamp::from_second(at).unwrap().to_string();
let (date, time) = stamp.split_once('T').unwrap();
let name = format!(
"rigger.v1-{}-{}.bak",
date.replace('-', ""),
time.chars().filter(char::is_ascii_digit).take(6).collect::<String>()
);
std::fs::write(data.join("profiles").join("line").join(&name), b"not a database, only a name").unwrap();
name
}
#[test]
fn backup_keeps_the_newest_copies_and_deletes_the_rest() {
let data = tempfile::tempdir().unwrap();
rigger(data.path()).arg("init").assert().success();
for _ in 0..4 {
rigger(data.path()).arg("backup").arg("--keep").arg("10").assert().success();
}
assert_eq!(copies(data.path()).len(), 4, "four copies must be four files: {:?}", copies(data.path()));
rigger(data.path())
.arg("backup")
.arg("--keep")
.arg("2")
.assert()
.success()
.stdout(predicate::str::contains("deleted"));
let left = copies(data.path());
assert_eq!(left.len(), 2, "--keep 2 must leave two copies, found {left:?}");
}
#[test]
fn a_copy_taken_in_the_same_second_is_the_newer_of_the_two() {
let data = tempfile::tempdir().unwrap();
rigger(data.path()).arg("init").assert().success();
for _ in 0..4 {
rigger(data.path()).arg("backup").arg("--keep").arg("10").assert().success();
}
let listed = rigger(data.path()).arg("backup").arg("--list").arg("--keep").arg("10").assert().success();
let out = String::from_utf8(listed.get_output().stdout.clone()).unwrap();
let first = out.lines().nth(1).unwrap().split_whitespace().next().unwrap().to_string();
let newest = copies(data.path()).first().unwrap().clone();
assert_eq!(
first, newest,
"the list must open with the newest copy
{out}"
);
rigger(data.path()).arg("backup").arg("--keep").arg("1").assert().success();
let left = copies(data.path());
assert_eq!(left.len(), 1, "expected one copy, found {left:?}");
rigger(data.path())
.arg("backup")
.arg("--list")
.assert()
.success()
.stdout(predicate::str::contains("unknown").not());
}
#[test]
fn keeping_none_still_keeps_the_copy_just_taken() {
let data = tempfile::tempdir().unwrap();
rigger(data.path()).arg("init").assert().success();
rigger(data.path()).arg("backup").assert().success();
rigger(data.path()).arg("backup").arg("--keep").arg("0").assert().success();
let left = copies(data.path());
assert_eq!(left.len(), 1, "`--keep 0` must keep the copy it just took, found {left:?}");
}
#[test]
fn doctor_reports_the_age_of_the_newest_copy() {
let data = tempfile::tempdir().unwrap();
rigger(data.path()).arg("init").assert().success();
rigger(data.path())
.arg("doctor")
.assert()
.success()
.stdout(predicate::str::contains("backup: none"));
copy_aged(data.path(), 3);
rigger(data.path())
.arg("doctor")
.assert()
.success()
.stdout(predicate::str::contains("3 days ago").and(predicate::str::contains("older than a day")));
let newer = copy_aged(data.path(), 3);
copy_aged(data.path(), 9);
rigger(data.path())
.arg("doctor")
.assert()
.success()
.stdout(predicate::str::contains("3 days ago"));
let day = newer.split('-').nth(1).expect("a copy's name carries the day it was taken").to_string();
for name in copies(data.path()).into_iter().filter(|n| n.contains(&day)) {
std::fs::remove_file(data.path().join("profiles").join("line").join(&name)).unwrap();
}
rigger(data.path())
.arg("doctor")
.assert()
.success()
.stdout(predicate::str::contains("older than a week"));
copy_aged(data.path(), 0);
rigger(data.path())
.arg("doctor")
.assert()
.success()
.stdout(predicate::str::contains("backup: today").and(predicate::str::contains("older than").not()));
}
#[test]
fn a_copy_whose_name_holds_no_moment_is_not_given_an_age() {
let data = tempfile::tempdir().unwrap();
rigger(data.path()).arg("init").assert().success();
std::fs::write(data.path().join("profiles").join("line").join("rigger.v1-20261345-080501.bak"), b"x").unwrap();
rigger(data.path())
.arg("doctor")
.assert()
.success()
.stdout(predicate::str::contains("backup: none"));
copy_aged(data.path(), 2);
rigger(data.path())
.arg("backup")
.arg("--list")
.assert()
.success()
.stdout(predicate::str::contains("20261345").and(predicate::str::contains("unknown")))
.stdout(predicate::str::contains("2 days ago"));
}
#[test]
fn ending_a_session_insures_the_record_when_the_newest_copy_is_a_day_old() {
let data = tempfile::tempdir().unwrap();
let root = data.path().join("here");
repo(&root);
rigger(data.path()).arg("init").assert().success();
rigger(data.path())
.arg("project")
.arg("add")
.arg(&root)
.arg("--name")
.arg("here")
.assert()
.success();
rigger(data.path()).arg("session").arg("start").arg("here").assert().success();
rigger(data.path())
.arg("session")
.arg("end")
.arg("here")
.assert()
.success()
.stdout(predicate::str::contains("copied the database"));
assert_eq!(copies(data.path()).len(), 1, "the close must leave a copy: {:?}", copies(data.path()));
rigger(data.path()).arg("session").arg("start").arg("here").assert().success();
rigger(data.path())
.arg("session")
.arg("end")
.arg("here")
.assert()
.success()
.stdout(predicate::str::contains("copied the database").not());
assert_eq!(copies(data.path()).len(), 1, "a fresh copy must not be taken twice a day");
for name in copies(data.path()) {
std::fs::remove_file(data.path().join("profiles").join("line").join(name)).unwrap();
}
copy_aged(data.path(), 2);
rigger(data.path()).arg("session").arg("start").arg("here").assert().success();
rigger(data.path())
.arg("session")
.arg("end")
.arg("here")
.assert()
.success()
.stdout(predicate::str::contains("copied the database"));
assert_eq!(copies(data.path()).len(), 2, "a stale copy must be joined by a fresh one");
}
fn with_project(data: &Path, name: &str) {
let root = data.join(name);
repo(&root);
rigger(data).arg("init").assert().success();
rigger(data).arg("project").arg("add").arg(&root).arg("--name").arg(name).assert().success();
}
#[test]
fn a_document_is_written_read_back_and_listed() {
let data = tempfile::tempdir().unwrap();
with_project(data.path(), "demo");
rigger(data.path())
.arg("doc")
.arg("list")
.arg("demo")
.assert()
.success()
.stdout(predicate::str::contains("no documents yet"));
rigger(data.path())
.args(["doc", "add", "demo", "Vision", "--kind", "vision", "--body", "# Vision\n\nBecause."])
.assert()
.success()
.stdout(predicate::str::contains("Wrote vision"));
let shown = rigger(data.path()).args(["doc", "show", "demo", "vision"]).assert().success();
let out = String::from_utf8(shown.get_output().stdout.clone()).unwrap();
assert_eq!(out.trim(), "# Vision\n\nBecause.", "show prints the body alone, got: {out:?}");
rigger(data.path())
.args(["doc", "list", "demo"])
.assert()
.success()
.stdout(predicate::str::contains("vision").and(predicate::str::contains("today")));
}
#[test]
fn a_second_document_of_a_singular_kind_is_refused() {
let data = tempfile::tempdir().unwrap();
with_project(data.path(), "demo");
rigger(data.path())
.args(["doc", "add", "demo", "Vision", "--kind", "vision", "--body", "one"])
.assert()
.success();
rigger(data.path())
.args(["doc", "add", "demo", "Another", "--kind", "vision", "--body", "two"])
.assert()
.failure()
.stderr(predicate::str::contains("already has a vision").and(predicate::str::contains("doc edit demo vision")));
rigger(data.path())
.args(["doc", "add", "demo", "First", "--kind", "research", "--body", "a"])
.assert()
.success();
rigger(data.path())
.args(["doc", "add", "demo", "Second", "--kind", "research", "--body", "b"])
.assert()
.success();
}
#[test]
fn a_title_with_no_ascii_still_gets_an_address_that_can_be_typed() {
let data = tempfile::tempdir().unwrap();
with_project(data.path(), "demo");
rigger(data.path())
.args(["doc", "add", "demo", "Исследование", "--kind", "research", "--body", "текст"])
.assert()
.success()
.stdout(predicate::str::contains("Wrote research"));
rigger(data.path())
.args(["doc", "add", "demo", "Ещё одно", "--kind", "research", "--body", "ещё"])
.assert()
.success()
.stdout(predicate::str::contains("Wrote research-2"));
rigger(data.path())
.args(["doc", "show", "demo", "research"])
.assert()
.success()
.stdout(predicate::str::contains("текст"));
}
#[test]
fn editing_replaces_the_body_and_keeps_the_day_it_was_started() {
let data = tempfile::tempdir().unwrap();
with_project(data.path(), "demo");
rigger(data.path())
.args(["doc", "add", "demo", "Vision", "--kind", "vision", "--body", "first"])
.assert()
.success();
let before = rigger(data.path()).args(["doc", "show", "demo", "vision", "--json"]).assert().success();
let before: serde_json::Value = serde_json::from_slice(&before.get_output().stdout).unwrap();
rigger(data.path())
.args(["doc", "edit", "demo", "vision", "--body", "second"])
.assert()
.success()
.stdout(predicate::str::contains("Wrote vision"));
let after = rigger(data.path()).args(["doc", "show", "demo", "vision", "--json"]).assert().success();
let after: serde_json::Value = serde_json::from_slice(&after.get_output().stdout).unwrap();
assert_eq!(after["body"], "second");
assert_eq!(after["created_at"], before["created_at"], "a rewrite must not restart the document's life");
assert_eq!(after["kind"], "vision");
rigger(data.path())
.args(["doc", "edit", "demo", "vision", "--title", "The vision"])
.assert()
.success();
let renamed = rigger(data.path()).args(["doc", "show", "demo", "vision", "--json"]).assert().success();
let renamed: serde_json::Value = serde_json::from_slice(&renamed.get_output().stdout).unwrap();
assert_eq!(renamed["title"], "The vision");
assert_eq!(renamed["body"], "second", "a rename must not touch the body");
}
#[test]
fn a_wrong_address_is_answered_with_the_addresses_there_are() {
let data = tempfile::tempdir().unwrap();
with_project(data.path(), "demo");
rigger(data.path())
.args(["doc", "show", "demo", "vision"])
.assert()
.failure()
.stderr(predicate::str::contains("none at all yet"));
rigger(data.path())
.args(["doc", "add", "demo", "Vision", "--kind", "vision", "--body", "x"])
.assert()
.success();
rigger(data.path())
.args(["doc", "show", "demo", "visoin"])
.assert()
.failure()
.stderr(predicate::str::contains("it has vision"));
}
#[test]
fn a_kind_rigger_does_not_know_is_refused_with_the_list() {
let data = tempfile::tempdir().unwrap();
with_project(data.path(), "demo");
rigger(data.path())
.args(["doc", "add", "demo", "Plan", "--kind", "plan", "--body", "x"])
.assert()
.failure()
.stderr(predicate::str::contains("vision").and(predicate::str::contains("research")));
}
#[test]
fn a_document_can_be_removed() {
let data = tempfile::tempdir().unwrap();
with_project(data.path(), "demo");
rigger(data.path()).args(["doc", "add", "demo", "Notes", "--body", "x"]).assert().success();
rigger(data.path())
.args(["doc", "remove", "demo", "notes"])
.assert()
.success()
.stdout(predicate::str::contains("Removed notes"));
rigger(data.path())
.args(["doc", "list", "demo"])
.assert()
.success()
.stdout(predicate::str::contains("no documents yet"));
}
#[test]
fn a_new_document_is_seeded_with_the_skeleton_of_its_kind() {
let data = tempfile::tempdir().unwrap();
with_project(data.path(), "demo");
rigger(data.path())
.args(["doc", "template", "vision"])
.assert()
.success()
.stdout(predicate::str::contains("## Why").and(predicate::str::contains("built-in skeleton")));
let shown = rigger(data.path())
.args(["doc", "add", "demo", "Vision of demo", "--kind", "vision"])
.env("RIGGER_EDITOR", editor_that_prints(data.path()))
.assert()
.success();
let seed = String::from_utf8(shown.get_output().stdout.clone()).unwrap();
assert!(seed.contains("# Vision of demo"), "the skeleton carries the title: {seed}");
assert!(seed.contains("## Why"), "the skeleton carries its questions: {seed}");
let research = rigger(data.path())
.args(["doc", "add", "demo", "A question", "--kind", "research"])
.env("RIGGER_EDITOR", editor_that_prints(data.path()))
.assert()
.success();
let seed = String::from_utf8(research.get_output().stdout.clone()).unwrap();
assert!(seed.contains("## The question"), "a research note asks its own questions: {seed}");
}
#[test]
fn a_skeleton_of_your_own_replaces_the_one_that_ships() {
let data = tempfile::tempdir().unwrap();
with_project(data.path(), "demo");
rigger(data.path())
.args(["doc", "template", "vision", "--write"])
.assert()
.success()
.stdout(predicate::str::contains("doc.vision.md"));
let written = data.path().join("profiles").join("line").join("doc.vision.md");
assert!(written.is_file(), "the skeleton must be written where it is read from");
rigger(data.path())
.args(["doc", "template", "vision", "--write"])
.assert()
.failure()
.stderr(predicate::str::contains("already exists"));
std::fs::write(&written, "# Видение {{title}}\n\n## 1. Зачем\n\n\n## 2. Главная идея\n").unwrap();
rigger(data.path())
.args(["doc", "template", "vision"])
.assert()
.success()
.stdout(predicate::str::contains("## 1. Зачем").and(predicate::str::contains("doc.vision.md")));
let shown = rigger(data.path())
.args(["doc", "add", "demo", "kilna", "--kind", "vision"])
.env("RIGGER_EDITOR", editor_that_prints(data.path()))
.assert()
.success();
let seed = String::from_utf8(shown.get_output().stdout.clone()).unwrap();
assert!(seed.contains("# Видение kilna"), "the placeholder becomes the title: {seed}");
assert!(!seed.contains("## Why"), "the built-in skeleton must be gone: {seed}");
let research = rigger(data.path())
.args(["doc", "add", "demo", "A question", "--kind", "research"])
.env("RIGGER_EDITOR", editor_that_prints(data.path()))
.assert()
.success();
let seed = String::from_utf8(research.get_output().stdout.clone()).unwrap();
assert!(seed.contains("## The question"), "an unoverridden kind keeps its skeleton: {seed}");
}
#[test]
fn two_processes_editing_the_same_document_name_do_not_share_a_scratch_file() {
let a = tempfile::tempdir().unwrap();
let b = tempfile::tempdir().unwrap();
with_project(a.path(), "demo");
with_project(b.path(), "demo");
let slow = editor_script(a.path(), "slow", "sleep 1; cat \"$1\"", "ping -n 2 127.0.0.1 >nul & type %1");
let mut first = std::process::Command::new(assert_cmd::cargo::cargo_bin("rigger"));
first
.env("RIGGER_DATA_DIR", a.path())
.env("RIGGER_EDITOR", slow)
.args(["doc", "add", "demo", "Vision", "--kind", "vision"])
.stdout(std::process::Stdio::piped());
let first = first.spawn().unwrap();
let overwrite = editor_script(
b.path(),
"overwrite",
"echo written by the other process > \"$1\"",
"echo written by the other process> %1",
);
rigger(b.path())
.args(["doc", "add", "demo", "Vision", "--kind", "vision"])
.env("RIGGER_EDITOR", overwrite)
.assert()
.success();
let out = first.wait_with_output().unwrap();
assert!(out.status.success(), "the first process must survive the second");
rigger(b.path())
.args(["doc", "show", "demo", "vision"])
.assert()
.success()
.stdout(predicate::str::contains("written by the other process"));
rigger(a.path())
.args(["doc", "show", "demo", "vision"])
.assert()
.success()
.stdout(predicate::str::contains("## Why").and(predicate::str::contains("written by the other process").not()));
}
fn editor_script(dir: &Path, name: &str, unix_body: &str, windows_body: &str) -> String {
if cfg!(windows) {
let script = dir.join(format!("{name}.cmd"));
std::fs::write(&script, format!("@echo off\r\n{windows_body}\r\n")).unwrap();
script.display().to_string()
} else {
let script = dir.join(format!("{name}.sh"));
std::fs::write(&script, format!("#!/bin/sh\n{unix_body}\n")).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
}
script.display().to_string()
}
}
fn editor_that_prints(dir: &Path) -> String {
editor_script(dir, "print", "cat \"$1\"", "type %1")
}
#[test]
fn a_generated_plan_does_not_reopen_a_task_the_record_closed() {
let data = tempfile::tempdir().unwrap();
with_project(data.path(), "demo");
let hub = data.path().join("hub");
std::fs::create_dir_all(&hub).unwrap();
let plan = "# План\n\n## v0.9.0 · Stage\n\n- [ ] first\n- [ ] second\n";
std::fs::write(hub.join("План.md"), plan).unwrap();
rigger(data.path()).args(["import", "demo", "--hub"]).arg(&hub).assert().success();
let by_hand = open_task_id(data.path(), "first");
rigger(data.path()).args(["task", "status", &by_hand, "done"]).assert().success();
rigger(data.path()).args(["import", "demo", "--hub"]).arg(&hub).assert().success();
assert!(
!task_is_done(data.path(), &by_hand),
"a hub kept by hand is the owner's pen: its empty box reopens the task"
);
std::fs::write(
hub.join("План.md"),
format!("<!-- generated by rigger; edits here are overwritten -->\n\n{plan}"),
)
.unwrap();
let generated = by_hand;
rigger(data.path()).args(["task", "status", &generated, "done"]).assert().success();
rigger(data.path()).args(["import", "demo", "--hub"]).arg(&hub).assert().success();
assert!(task_is_done(data.path(), &generated), "a generated plan must not reopen what the record closed");
}
fn open_task_id(data: &Path, title: &str) -> String {
let out = rigger(data).args(["context", "demo", "--json"]).assert().success();
let packet: serde_json::Value = serde_json::from_slice(&out.get_output().stdout).unwrap();
packet["current"]["tasks"]
.as_array()
.unwrap()
.iter()
.find(|t| t["title"].as_str().unwrap().contains(title))
.map(|t| t["id"].as_i64().unwrap().to_string())
.unwrap_or_else(|| panic!("no task titled {title} in {packet}"))
}
fn task_is_done(data: &Path, id: &str) -> bool {
let out = rigger(data).args(["task", "status", id, "done"]).assert().success();
let said = String::from_utf8(out.get_output().stdout.clone()).unwrap();
said.contains("was already done")
}
#[test]
fn importing_a_hub_takes_in_its_handwritten_texts() {
let data = tempfile::tempdir().unwrap();
with_project(data.path(), "demo");
let hub = data.path().join("hub");
std::fs::create_dir_all(hub.join("Исследования")).unwrap();
std::fs::write(hub.join("Видение.md"), "# Видение demo\n\n## 1. Зачем\n\nЗатем.\n").unwrap();
std::fs::write(hub.join("Ритуалы.md"), "# Ритуалы demo\n\nПравила.\n").unwrap();
std::fs::write(
hub.join("Решения.md"),
"# Журнал решений\n\nappend-only, новые сверху.\n\n---\n\n## 2026-09-11 · Что-то\n\nТекст решения.\n",
)
.unwrap();
std::fs::write(hub.join("Исследования").join("2026-09-04 — Инструмент.md"), "# Инструмент\n\nЧто?\n").unwrap();
rigger(data.path())
.args(["import", "demo", "--hub"])
.arg(&hub)
.assert()
.success()
.stdout(predicate::str::contains("documents 4 added"));
let listed = rigger(data.path()).args(["doc", "list", "demo"]).assert().success();
let out = String::from_utf8(listed.get_output().stdout.clone()).unwrap();
for slug in ["vision", "rituals", "decisions", "2026-09-04"] {
assert!(out.contains(slug), "{slug} must be in the list:\n{out}");
}
let shown = rigger(data.path()).args(["doc", "show", "demo", "decisions"]).assert().success();
let preamble = String::from_utf8(shown.get_output().stdout.clone()).unwrap();
assert!(preamble.contains("append-only"), "the preamble is the document: {preamble}");
assert!(
!preamble.contains("Текст решения"),
"an entry is an event, not part of the document: {preamble}"
);
}
#[test]
fn importing_the_same_hub_twice_changes_nothing_the_second_time() {
let data = tempfile::tempdir().unwrap();
with_project(data.path(), "demo");
let hub = data.path().join("hub");
std::fs::create_dir_all(&hub).unwrap();
std::fs::write(hub.join("Видение.md"), "# Видение demo\n\nТекст.\n").unwrap();
rigger(data.path()).args(["import", "demo", "--hub"]).arg(&hub).assert().success();
rigger(data.path())
.args(["import", "demo", "--hub"])
.arg(&hub)
.assert()
.success()
.stdout(predicate::str::contains("nothing changed"));
std::fs::write(hub.join("Видение.md"), "# Видение demo\n\nДругой текст.\n").unwrap();
rigger(data.path())
.args(["import", "demo", "--hub"])
.arg(&hub)
.assert()
.success()
.stdout(predicate::str::contains("documents 0 added, 1 updated"));
}
#[test]
fn two_research_notes_from_one_day_both_survive_the_import() {
let data = tempfile::tempdir().unwrap();
with_project(data.path(), "demo");
let notes = data.path().join("hub").join("Исследования");
std::fs::create_dir_all(¬es).unwrap();
std::fs::write(notes.join("2026-09-04 — Первое.md"), "# Первое\n\nA\n").unwrap();
std::fs::write(notes.join("2026-09-04 — Второе.md"), "# Второе\n\nB\n").unwrap();
std::fs::write(notes.join("2026-09-05 — Третье.md"), "# Третье\n\nC\n").unwrap();
rigger(data.path())
.args(["import", "demo", "--hub"])
.arg(data.path().join("hub"))
.assert()
.success()
.stdout(predicate::str::contains("documents 3 added"));
let listed = rigger(data.path()).args(["doc", "list", "demo", "--json"]).assert().success();
let docs: serde_json::Value = serde_json::from_slice(&listed.get_output().stdout).unwrap();
let titles: Vec<&str> = docs.as_array().unwrap().iter().map(|d| d["title"].as_str().unwrap()).collect();
assert_eq!(titles.len(), 3, "every note must survive: {titles:?}");
for title in ["Первое", "Второе", "Третье"] {
assert!(titles.contains(&title), "{title} was lost: {titles:?}");
}
}
#[test]
fn a_placeholder_under_an_empty_queue_is_not_a_question() {
let data = tempfile::tempdir().unwrap();
with_project(data.path(), "demo");
let hub = data.path().join("hub");
std::fs::create_dir_all(&hub).unwrap();
std::fs::write(
hub.join("План.md"),
"# План\n\n## Ждёт решения владельца\n\n- (пусто) — модель решается замером (v0.6).\n",
)
.unwrap();
rigger(data.path()).args(["import", "demo", "--hub"]).arg(&hub).assert().success();
rigger(data.path())
.args(["inbox"])
.assert()
.success()
.stdout(predicate::str::contains("Nothing is waiting on you").or(predicate::str::contains("пусто").not()));
}
#[test]
fn a_question_that_opens_with_a_bracket_keeps_it() {
let data = tempfile::tempdir().unwrap();
with_project(data.path(), "demo");
let hub = data.path().join("hub");
std::fs::create_dir_all(&hub).unwrap();
std::fs::write(
hub.join("План.md"),
"# План\n\n## Ждёт решения владельца\n\n- (в работе) стоит ли поднимать мажор\n",
)
.unwrap();
rigger(data.path()).args(["import", "demo", "--hub"]).arg(&hub).assert().success();
rigger(data.path())
.args(["inbox"])
.assert()
.success()
.stdout(predicate::str::contains("(в работе)"));
}
#[test]
fn checking_an_export_shows_what_would_be_lost() {
let data = tempfile::tempdir().unwrap();
with_project(data.path(), "demo");
let hub = data.path().join("hub");
std::fs::create_dir_all(&hub).unwrap();
std::fs::write(hub.join("План.md"), "# План\n\n## v0.9.0 · Stage\n\n- [ ] first\n").unwrap();
rigger(data.path()).args(["import", "demo", "--hub"]).arg(&hub).assert().success();
rigger(data.path()).args(["export", "demo", "--adopt", "--hub"]).arg(&hub).assert().success();
let mut plan = std::fs::read_to_string(hub.join("План.md")).unwrap();
plan.push_str("\nРукописная строка.\n");
std::fs::write(hub.join("План.md"), plan).unwrap();
rigger(data.path())
.args(["export", "demo", "--check", "--hub"])
.arg(&hub)
.assert()
.success()
.stdout(predicate::str::contains("would change").and(predicate::str::contains("- Рукописная строка.")));
assert!(
std::fs::read_to_string(hub.join("План.md")).unwrap().contains("Рукописная строка."),
"--check must not write"
);
}
#[test]
fn exporting_documents_writes_files_that_import_reads_back_unchanged() {
let data = tempfile::tempdir().unwrap();
with_project(data.path(), "demo");
let hub = data.path().join("hub");
std::fs::create_dir_all(hub.join("Исследования")).unwrap();
std::fs::write(hub.join("Видение.md"), "# Видение demo\n\nТекст.\n").unwrap();
std::fs::write(hub.join("Ритуалы.md"), "# Ритуалы demo\n\nПравила.\n").unwrap();
std::fs::write(hub.join("Исследования").join("2026-09-04 — Заметка.md"), "# Заметка о другом\n\nТело.\n").unwrap();
rigger(data.path()).args(["import", "demo", "--hub"]).arg(&hub).assert().success();
let out = data.path().join("out");
std::fs::create_dir_all(&out).unwrap();
rigger(data.path())
.args(["export", "demo", "--adopt", "--docs", "--hub"])
.arg(&out)
.assert()
.success();
assert!(out.join("Видение.md").is_file(), "the vision must be written");
assert!(out.join("Ритуалы.md").is_file(), "the rituals must be written");
assert!(
out.join("Исследования").join("2026-09-04 — Заметка.md").is_file(),
"a research note keeps the date it is addressed by: {:?}",
std::fs::read_dir(out.join("Исследования"))
.unwrap()
.flatten()
.map(|e| e.file_name())
.collect::<Vec<_>>()
);
let names: Vec<String> = std::fs::read_dir(out.join("Исследования"))
.unwrap()
.flatten()
.map(|e| e.file_name().to_string_lossy().to_string())
.collect();
assert_eq!(names.len(), 1, "one note must not become two files: {names:?}");
rigger(data.path())
.args(["import", "demo", "--hub"])
.arg(&out)
.assert()
.success()
.stdout(predicate::str::contains("nothing changed"));
let bare = data.path().join("bare");
std::fs::create_dir_all(&bare).unwrap();
rigger(data.path()).args(["export", "demo", "--adopt", "--hub"]).arg(&bare).assert().success();
assert!(!bare.join("Видение.md").exists(), "documents are written only when asked for");
}
#[test]
fn the_command_tree_is_walked_without_overflowing_the_stack() {
Command::cargo_bin("rigger").unwrap().arg("--version").assert().success();
Command::cargo_bin("rigger").unwrap().arg("--help").assert().success();
Command::cargo_bin("rigger").unwrap().args(["doc", "add", "--help"]).assert().success();
}
#[test]
fn a_usage_error_never_exits_with_the_code_that_blocks_a_hook() {
let data = tempfile::tempdir().unwrap();
for args in [vec!["no-such-command"], vec!["session", "end", "--no-such-flag"], vec!["project"]] {
let out = rigger(data.path()).args(&args).assert().failure();
let code = out.get_output().status.code();
assert_eq!(code, Some(1), "{args:?} exited {code:?}, and 2 blocks a Stop hook");
}
for args in [vec!["--help"], vec!["--version"]] {
rigger(data.path()).args(&args).assert().success();
}
}
#[test]
fn doctor_reports_the_database_before_and_after_init() {
let data = tempfile::tempdir().unwrap();
rigger(data.path())
.arg("doctor")
.assert()
.success()
.stdout(predicate::str::contains("missing - run `rigger init`"));
let root = data.path().join("one");
repo(&root);
rigger(data.path()).arg("init").assert().success();
rigger(data.path()).args(["project", "add"]).arg(&root).assert().success();
rigger(data.path())
.arg("doctor")
.assert()
.success()
.stdout(predicate::str::contains("schema: version").and(predicate::str::contains("projects: 1")));
rigger(data.path())
.args(["doctor", "--json"])
.assert()
.success()
.stdout(predicate::str::contains("\"schema_version\":"));
}