use assert_cmd::Command;
use predicates::boolean::PredicateBooleanExt;
use predicates::str;
use std::path::PathBuf;
fn fixtures() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
}
fn cmd() -> Command {
let mut c = Command::cargo_bin("repolith").expect("repolith binary must be built");
c.env_remove("RUST_LOG");
c
}
#[test]
fn version_flag_prints_semver() {
cmd()
.arg("--version")
.assert()
.success()
.stdout(str::contains("repolith"));
}
#[test]
fn help_lists_all_subcommands() {
cmd()
.arg("--help")
.assert()
.success()
.stdout(str::contains("sync"))
.stdout(str::contains("status"));
}
#[test]
fn dry_run_empty_manifest_reports_zero() {
let tmp = tempfile::tempdir().unwrap();
cmd()
.arg("--manifest")
.arg(fixtures().join("empty.toml"))
.arg("--cache-path")
.arg(tmp.path().join("cache.db"))
.arg("sync")
.arg("--dry-run")
.assert()
.success()
.stdout(str::contains("0 action(s) would run"));
}
#[test]
fn dry_run_minimal_manifest_reports_one() {
let tmp = tempfile::tempdir().unwrap();
cmd()
.arg("--manifest")
.arg(fixtures().join("minimal.toml"))
.arg("--cache-path")
.arg(tmp.path().join("cache.db"))
.arg("sync")
.arg("--dry-run")
.assert()
.success()
.stdout(str::contains("1 action(s) would run"));
}
#[test]
fn explain_prints_change_reason() {
let tmp = tempfile::tempdir().unwrap();
cmd()
.arg("--manifest")
.arg(fixtures().join("minimal.toml"))
.arg("--cache-path")
.arg(tmp.path().join("cache.db"))
.arg("sync")
.arg("--explain")
.arg("--dry-run")
.assert()
.success()
.stdout(str::contains("never built"));
}
#[test]
fn invalid_schema_returns_error() {
let tmp = tempfile::tempdir().unwrap();
cmd()
.arg("--manifest")
.arg(fixtures().join("invalid_schema.toml"))
.arg("--cache-path")
.arg(tmp.path().join("cache.db"))
.arg("sync")
.arg("--dry-run")
.assert()
.failure()
.stderr(str::contains("schema"));
}
#[test]
fn status_prints_table_with_action_row() {
let tmp = tempfile::tempdir().unwrap();
cmd()
.arg("--manifest")
.arg(fixtures().join("minimal.toml"))
.arg("--cache-path")
.arg(tmp.path().join("cache.db"))
.arg("status")
.assert()
.success()
.stdout(str::contains("local-thing::cargo-install::0"))
.stdout(str::contains("stale"));
}
#[test]
fn status_without_filter_still_renders_a_table() {
let tmp = tempfile::tempdir().unwrap();
cmd()
.arg("--manifest")
.arg(fixtures().join("two_nodes.toml"))
.arg("--cache-path")
.arg(tmp.path().join("cache.db"))
.arg("status")
.assert()
.success()
.stdout(str::contains("Action"))
.stdout(str::contains("Reason"))
.stdout(str::contains("+---"))
.stdout(str::contains("state ").not());
}
#[test]
fn status_with_filter_prints_a_detail_block() {
let tmp = tempfile::tempdir().unwrap();
cmd()
.arg("--manifest")
.arg(fixtures().join("two_nodes.toml"))
.arg("--cache-path")
.arg(tmp.path().join("cache.db"))
.arg("status")
.arg("beta")
.assert()
.success()
.stdout(str::contains("beta::cargo-install::0"))
.stdout(str::contains("state"))
.stdout(str::contains("last run"))
.stdout(str::contains("action"))
.stdout(str::contains("profile"))
.stdout(str::contains("alpha").not());
}
#[test]
fn status_filter_matches_every_action_of_a_node() {
let tmp = tempfile::tempdir().unwrap();
cmd()
.arg("--manifest")
.arg(fixtures().join("two_nodes.toml"))
.arg("--cache-path")
.arg(tmp.path().join("cache.db"))
.arg("status")
.arg("alpha")
.assert()
.success()
.stdout(str::contains("alpha::cargo-install::0"))
.stdout(str::contains("alpha-pkg"))
.stdout(str::contains("dev"))
.stdout(str::contains("extra"))
.stdout(str::contains("deps"));
}
#[test]
fn status_filter_matching_nothing_exits_nonzero() {
let tmp = tempfile::tempdir().unwrap();
cmd()
.arg("--manifest")
.arg(fixtures().join("two_nodes.toml"))
.arg("--cache-path")
.arg(tmp.path().join("cache.db"))
.arg("status")
.arg("gamma")
.assert()
.failure()
.stderr(str::contains("gamma"));
}
fn local_repo(at: &std::path::Path) -> PathBuf {
let repo = at.join("origin");
std::fs::create_dir_all(&repo).unwrap();
let git = |args: &[&str]| {
let out = std::process::Command::new("git")
.args(args)
.current_dir(&repo)
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.output()
.expect("git must be on PATH");
assert!(out.status.success(), "git {args:?}: {out:?}");
};
git(&["init", "--quiet", "--initial-branch=main"]);
std::fs::write(repo.join("README.md"), "not a crate\n").unwrap();
git(&["add", "."]);
git(&[
"-c",
"user.email=t@example.com",
"-c",
"user.name=t",
"commit",
"--quiet",
"-m",
"init",
]);
repo
}
#[test]
fn status_detail_shows_the_full_error_and_the_dependency_edge() {
let tmp = tempfile::tempdir().unwrap();
let cache = tmp.path().join("cache.db");
let manifest = tmp.path().join("failing.toml");
let origin = local_repo(tmp.path());
std::fs::write(
&manifest,
format!(
r#"
[orchestrator]
schema_version = "0.1"
name = "failing"
[[node]]
id = "nope"
git = "file://{}"
path = "{}"
[[node.action]]
kind = "git-clone"
[[node.action]]
kind = "cargo-install"
crate = "nope"
"#,
origin.display(),
tmp.path().join("checkout").display()
),
)
.unwrap();
cmd()
.arg("--manifest")
.arg(&manifest)
.arg("--cache-path")
.arg(&cache)
.arg("sync")
.assert()
.failure();
cmd()
.arg("--manifest")
.arg(&manifest)
.arg("--cache-path")
.arg(&cache)
.arg("status")
.arg("nope::cargo-install")
.assert()
.success()
.stdout(str::contains("nope::cargo-install::1"))
.stdout(str::contains("previous run failed"))
.stdout(str::contains("last run"))
.stdout(str::contains("nope::git-clone::0"))
.stdout(str::contains("date not recorded").not());
}
#[test]
fn status_help_documents_the_filter() {
cmd()
.arg("status")
.arg("--help")
.assert()
.success()
.stdout(str::contains("FILTER"));
}