use assert_cmd::prelude::*;
use predicates::prelude::*;
use std::process::Command;
use std::sync::Arc;
use sysknife_daemon::audit_chain::{AuditKey, ChainRow};
use sysknife_daemon::auth::CallerPrincipal;
use sysknife_daemon::transactions::{NewTransaction, TransactionStore};
use sysknife_types::{CallerRole, RiskLevel};
fn fake_socket(dir: &tempfile::TempDir) -> std::path::PathBuf {
dir.path().join("does-not-exist.sock")
}
fn cli() -> Command {
Command::cargo_bin("sysknife").expect("sysknife binary builds")
}
#[test]
fn help_lists_every_top_level_subcommand() {
let output = cli()
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("doctor"))
.stdout(predicate::str::contains("history"))
.stdout(predicate::str::contains("audit"))
.stdout(predicate::str::contains("mcp-server"))
.stdout(predicate::str::contains("completions"));
drop(output);
}
#[test]
fn unknown_subcommand_via_clap_usage_error() {
cli()
.arg("--no-such-flag")
.assert()
.failure()
.stderr(predicate::str::contains("unexpected").or(predicate::str::contains("unknown")));
}
#[test]
fn doctor_fails_loudly_when_daemon_socket_is_unreachable() {
let dir = tempfile::tempdir().unwrap();
cli()
.env("SYSKNIFE_SOCKET", fake_socket(&dir))
.arg("doctor")
.assert()
.failure()
.stderr(predicate::str::contains("daemon"))
.stdout(predicate::str::is_empty());
}
#[test]
fn doctor_json_failure_goes_to_stdout_not_stderr() {
let dir = tempfile::tempdir().unwrap();
cli()
.env("SYSKNIFE_SOCKET", fake_socket(&dir))
.args(["doctor", "--json"])
.assert()
.failure()
.stdout(predicate::str::contains("\"ok\":false"));
}
#[test]
fn history_rejects_unparseable_since_timestamp() {
let dir = tempfile::tempdir().unwrap();
cli()
.env("SYSKNIFE_SOCKET", fake_socket(&dir))
.arg("history")
.arg("--since")
.arg("not-a-timestamp")
.assert()
.code(4);
}
#[test]
fn audit_verify_exits_with_code_2_when_the_key_file_is_missing() {
let dir = tempfile::tempdir().unwrap();
cli()
.env("SYSKNIFE_SOCKET", fake_socket(&dir))
.env("XDG_DATA_HOME", dir.path())
.env("HOME", dir.path())
.args(["audit", "verify", "--pubkey"])
.arg(dir.path().join("no-such-key.pub"))
.assert()
.code(2);
}
#[test]
fn audit_export_emits_the_stored_rows_as_parseable_json() {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("daemon.sqlite");
let key = Arc::new(AuditKey::load_or_generate(&dir.path().join("audit-key")).unwrap());
let store = TransactionStore::open_with_key(&db_path, key).unwrap();
for request_id in ["req-1", "req-2"] {
store
.record(NewTransaction {
request_id: request_id.to_string(),
request_hash: format!("hash-{request_id}"),
action_name: "UpdateSystem".to_string(),
risk_level: RiskLevel::High,
summary: "Upgrade the system".to_string(),
warnings: vec![],
caller_role: CallerRole::Dev,
caller_principal: CallerPrincipal::Uid(1000),
})
.unwrap();
}
let stored = store.fetch_chain_rows().unwrap();
drop(store);
let output = cli()
.env("SYSKNIFE_DATABASE_PATH", &db_path)
.env("HOME", dir.path())
.env("XDG_CONFIG_HOME", dir.path())
.args(["audit", "export", "--limit", "1"])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let exported: Vec<ChainRow> = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(exported.len(), 1);
assert_eq!(exported[0], stored[0]);
assert_eq!(
exported[0].chain_hash.as_bytes(),
stored[0].chain_hash.as_bytes()
);
}
#[test]
fn audit_export_rejects_an_invalid_since_timestamp() {
let dir = tempfile::tempdir().unwrap();
cli()
.env("SYSKNIFE_DATABASE_PATH", dir.path().join("unused.sqlite"))
.env("HOME", dir.path())
.env("XDG_CONFIG_HOME", dir.path())
.args(["audit", "export", "--since", "not-a-timestamp"])
.assert()
.code(4)
.stderr(predicate::str::contains("--since"));
}
#[test]
fn completions_subcommand_emits_a_shell_script() {
cli()
.arg("completions")
.arg("bash")
.assert()
.success()
.stdout(predicate::str::contains("_sysknife").or(predicate::str::starts_with("#")));
}
#[test]
fn timeout_flag_bounds_a_daemon_that_accepts_but_never_replies() {
use std::os::unix::net::UnixListener;
let dir = tempfile::tempdir().unwrap();
let socket_path = dir.path().join("silent.sock");
let listener = UnixListener::bind(&socket_path).expect("bind test socket");
std::thread::spawn(move || {
let mut held = Vec::new();
while let Ok((stream, _)) = listener.accept() {
held.push(stream);
}
});
let started = std::time::Instant::now();
cli()
.env("SYSKNIFE_SOCKET", &socket_path)
.args(["--timeout", "1", "doctor"])
.assert()
.code(2)
.stderr(predicate::str::contains("timed out"));
let elapsed = started.elapsed();
assert!(
elapsed < std::time::Duration::from_secs(30),
"--timeout 1 must give up promptly against a silent daemon, took {elapsed:?}"
);
}
#[test]
#[cfg(unix)]
fn audit_export_distinguishes_unreadable_from_absent() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let store_dir = dir.path().join("state");
std::fs::create_dir_all(&store_dir).unwrap();
let db = store_dir.join("daemon.sqlite");
std::fs::write(&db, b"not really a database").unwrap();
std::fs::set_permissions(&store_dir, std::fs::Permissions::from_mode(0o000)).unwrap();
let unreadable = std::fs::metadata(&db).is_err();
let assertion = if unreadable {
Some(
cli()
.env("SYSKNIFE_DATABASE_PATH", &db)
.env("SYSKNIFE_SOCKET", fake_socket(&dir))
.args(["audit", "export"])
.assert()
.failure(),
)
} else {
None
};
std::fs::set_permissions(&store_dir, std::fs::Permissions::from_mode(0o755)).unwrap();
match assertion {
Some(a) => {
a.stderr(
predicate::str::contains("not readable")
.or(predicate::str::contains("Permission denied")),
);
}
None => {
eprintln!("skipped: this user stats through mode 000 (probably root)");
}
}
}
#[test]
fn the_skip_approval_flag_alone_refuses_and_names_the_second_key() {
let dir = tempfile::tempdir().unwrap();
cli()
.env("SYSKNIFE_SOCKET", fake_socket(&dir))
.env_remove("SYSKNIFE_I_ACCEPT_UNATTENDED_ROOT")
.args(["--dangerously-skip-approval", "doctor"])
.assert()
.failure()
.stderr(
predicate::str::contains("SYSKNIFE_I_ACCEPT_UNATTENDED_ROOT")
.and(predicate::str::contains("as root")),
);
}
#[test]
fn a_non_exact_consent_value_does_not_arm_the_flag() {
let dir = tempfile::tempdir().unwrap();
for value in ["0", "false", "true", "yes", ""] {
cli()
.env("SYSKNIFE_SOCKET", fake_socket(&dir))
.env("SYSKNIFE_I_ACCEPT_UNATTENDED_ROOT", value)
.args(["--dangerously-skip-approval", "doctor"])
.assert()
.failure()
.stderr(predicate::str::contains(
"SYSKNIFE_I_ACCEPT_UNATTENDED_ROOT",
));
}
}
#[test]
fn both_keys_print_the_banner_and_let_the_command_run() {
let dir = tempfile::tempdir().unwrap();
cli()
.env("SYSKNIFE_SOCKET", fake_socket(&dir))
.env("SYSKNIFE_I_ACCEPT_UNATTENDED_ROOT", "1")
.args(["--dangerously-skip-approval", "doctor"])
.assert()
.stderr(
predicate::str::contains("UNATTENDED MODE")
.and(predicate::str::contains("approval gate is OFF"))
.and(predicate::str::contains("polkit allowlist"))
.and(predicate::str::contains("is not set to").not()),
);
}
#[test]
fn the_consent_variable_alone_is_inert() {
let dir = tempfile::tempdir().unwrap();
cli()
.env("SYSKNIFE_SOCKET", fake_socket(&dir))
.env("SYSKNIFE_I_ACCEPT_UNATTENDED_ROOT", "1")
.args(["doctor"])
.assert()
.stderr(predicate::str::contains("UNATTENDED MODE").not());
}
#[test]
fn help_says_what_the_skip_approval_flag_does_and_does_not_do() {
cli()
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("--dangerously-skip-approval"));
cli().arg("--help").assert().success().stdout(
predicate::str::contains("SYSKNIFE_I_ACCEPT_UNATTENDED_ROOT")
.or(predicate::str::contains("Requires")),
);
}