use assert_cmd::Command;
use predicates::prelude::*;
const SUBCOMMANDS: &[&str] = &[
"summary",
"hotspots",
"overhead",
"compare",
"state",
"ingest",
"mcp-server",
];
const UNIMPLEMENTED_SUBCOMMANDS: &[&str] = &[];
fn burn() -> Command {
Command::cargo_bin("burn").expect("`burn` binary must build for the smoke test")
}
#[test]
fn top_level_help_lists_every_subcommand() {
let output = burn().arg("--help").assert().success().get_output().clone();
let stdout = String::from_utf8(output.stdout).expect("help should be valid UTF-8");
assert!(!stdout.is_empty(), "--help must emit non-empty stdout");
for sub in SUBCOMMANDS {
assert!(
stdout.contains(sub),
"expected `--help` to mention subcommand `{sub}`; got:\n{stdout}",
);
}
assert!(
!stdout
.lines()
.any(|line| line.trim_start().starts_with("run ")),
"`burn --help` must not advertise removed `run` command; got:\n{stdout}",
);
}
#[test]
fn each_subcommand_help_exits_zero_with_non_empty_stdout() {
for sub in SUBCOMMANDS {
let output = burn()
.args([sub, "--help"])
.assert()
.success()
.get_output()
.clone();
let stdout = String::from_utf8(output.stdout).expect("help should be valid UTF-8");
assert!(
!stdout.is_empty(),
"`{sub} --help` should emit non-empty stdout; got empty",
);
}
}
#[test]
fn overhead_trim_help_exits_zero_with_non_empty_stdout() {
let output = burn()
.args(["overhead", "trim", "--help"])
.assert()
.success()
.get_output()
.clone();
let stdout = String::from_utf8(output.stdout).expect("help should be valid UTF-8");
assert!(
!stdout.is_empty(),
"`overhead trim --help` should emit non-empty stdout; got empty",
);
}
#[test]
fn each_stub_exits_one_with_not_yet_implemented_message() {
for sub in UNIMPLEMENTED_SUBCOMMANDS {
burn()
.arg(sub)
.assert()
.code(1)
.stderr(predicate::str::contains("not yet implemented"));
}
}
#[test]
fn compare_command_rejects_missing_models() {
burn()
.arg("compare")
.assert()
.code(2)
.stderr(predicate::str::contains("needs at least 2 models"));
}
#[test]
fn json_mode_emits_error_envelope_on_argument_failure() {
let output = burn()
.args(["--json", "compare"])
.assert()
.code(2)
.get_output()
.clone();
let stdout = String::from_utf8(output.stdout).expect("stdout should be valid UTF-8");
assert!(
stdout.contains("\"error\""),
"expected JSON-mode envelope on stdout; got:\n{stdout}",
);
assert!(
stdout.contains("needs at least 2 models"),
"expected JSON-mode envelope to carry the compare error message; got:\n{stdout}",
);
}
#[test]
fn version_flag_exits_zero() {
burn()
.arg("--version")
.assert()
.success()
.stdout(predicate::str::is_empty().not());
}
#[test]
fn unknown_subcommand_exits_non_zero() {
burn()
.arg("definitely-not-a-real-subcommand")
.assert()
.failure();
}
#[test]
fn run_subcommand_is_not_registered() {
burn().args(["run", "--help"]).assert().failure();
}
#[test]
fn hotspots_session_without_id_is_an_explicit_stub() {
burn()
.args(["hotspots", "--session"])
.assert()
.code(2)
.stderr(predicate::str::contains(
"per-session aggregate view (`--session` with no id)",
));
}
#[test]
fn hotspots_explain_drift_is_an_explicit_stub() {
burn()
.args(["hotspots", "--explain-drift"])
.assert()
.code(2)
.stderr(predicate::str::contains("--explain-drift"));
}
#[test]
fn hotspots_unknown_pattern_value_is_rejected() {
burn()
.args(["hotspots", "--patterns", "definitely-not-a-detector"])
.assert()
.code(2)
.stderr(predicate::str::contains("unknown --patterns value"));
}
#[test]
fn hotspots_group_by_and_patterns_are_mutually_exclusive() {
burn()
.args([
"hotspots",
"--group-by",
"file",
"--patterns",
"retry-loop",
])
.assert()
.code(2)
.stderr(predicate::str::contains("mutually exclusive"));
}
#[test]
fn state_reset_dry_run_does_not_mutate() {
let home = tempfile::TempDir::new().expect("tmp RELAYBURN_HOME");
burn()
.args(["state", "reset"])
.env("RELAYBURN_HOME", home.path())
.env("HOME", home.path())
.env("NO_COLOR", "1")
.assert()
.success()
.stdout(predicate::str::contains("dry run"))
.stdout(predicate::str::contains("--force"));
assert!(
home.path().join("burn.sqlite").is_file(),
"burn.sqlite must exist after dry-run open"
);
assert!(
home.path().join("content.sqlite").is_file(),
"content.sqlite must exist after dry-run open"
);
}
#[test]
fn state_reset_force_emits_executed_envelope() {
let home = tempfile::TempDir::new().expect("tmp RELAYBURN_HOME");
let output = burn()
.args(["--json", "state", "reset", "--force"])
.env("RELAYBURN_HOME", home.path())
.env("HOME", home.path())
.env("NO_COLOR", "1")
.assert()
.success()
.get_output()
.clone();
let stdout = String::from_utf8(output.stdout).expect("utf-8 stdout");
let value: serde_json::Value =
serde_json::from_str(stdout.trim()).expect("--json output is valid JSON");
assert_eq!(value["executed"], serde_json::Value::Bool(true));
assert_eq!(value["rowsDropped"], serde_json::Value::from(0));
assert_eq!(value["stampsDropped"], serde_json::Value::from(0));
assert_eq!(value["contentRowsDropped"], serde_json::Value::from(0));
assert!(
value.get("reingest").is_none(),
"no `reingest` key without --reingest"
);
}
#[test]
fn state_reset_reingest_requires_force() {
burn()
.args(["state", "reset", "--reingest"])
.assert()
.failure();
}