use std::path::PathBuf;
use std::process::Command;
fn binary() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_keyhog"))
}
fn run(args: &[&str]) -> (Option<i32>, String, String) {
let output = Command::new(binary())
.args(args)
.output()
.unwrap_or_else(|e| panic!("spawn keyhog {args:?}: {e}"));
(
output.status.code(),
String::from_utf8_lossy(&output.stdout).into_owned(),
String::from_utf8_lossy(&output.stderr).into_owned(),
)
}
fn usage_tokens(stdout: &str) -> Vec<String> {
let line = stdout
.lines()
.find(|l| l.starts_with("Usage:"))
.unwrap_or_else(|| panic!("help output has no `Usage:` line; got:\n{stdout}"));
line.split_whitespace().map(|t| t.to_string()).collect()
}
#[test]
fn version_first_line_equals_cargo_pkg_version() {
let (code, stdout, stderr) = run(&["--version"]);
assert_eq!(code, Some(0), "--version must exit 0; stderr={stderr}");
let first = stdout
.lines()
.next()
.expect("--version must print a first line");
let expected = format!("KeyHog v{}", env!("CARGO_PKG_VERSION"));
assert_eq!(
first, expected,
"--version line 1 must equal `{expected}`; got {first:?}"
);
}
#[test]
fn version_string_is_strict_numeric_semver() {
let ver = env!("CARGO_PKG_VERSION");
assert!(
!ver.contains('-'),
"version must carry no pre-release suffix; got {ver:?}"
);
assert!(
!ver.contains('+'),
"version must carry no build-metadata suffix; got {ver:?}"
);
let parts: Vec<&str> = ver.split('.').collect();
assert_eq!(
parts.len(),
3,
"version must have exactly 3 dot-separated components; got {ver:?}"
);
for (idx, part) in parts.iter().enumerate() {
assert!(
!part.is_empty() && part.bytes().all(|b| b.is_ascii_digit()),
"version component {idx} must be a non-empty ASCII-digit run; got {part:?} in {ver:?}"
);
}
let (_, stdout, _) = run(&["--version"]);
let first = stdout.lines().next().expect("first line");
let printed = first
.strip_prefix("KeyHog v")
.expect("first line begins with `KeyHog v`");
assert_eq!(
printed, ver,
"binary-reported semver must equal CARGO_PKG_VERSION"
);
}
#[test]
fn short_and_long_version_flags_are_byte_identical() {
let (code_long, out_long, _e1) = run(&["--version"]);
let (code_short, out_short, _e2) = run(&["-V"]);
assert_eq!(code_long, Some(0), "--version must exit 0");
assert_eq!(code_short, Some(0), "-V must exit 0");
assert_eq!(
out_short, out_long,
"-V and --version must print byte-identical stdout"
);
}
#[test]
fn version_commit_line_matches_core_git_hash() {
let (code, stdout, _e) = run(&["--version"]);
assert_eq!(code, Some(0), "--version must exit 0");
let commit_line = stdout
.lines()
.nth(1)
.expect("--version must print a second line");
let expected = format!("Commit: {}", keyhog_core::git_hash());
assert_eq!(
commit_line, expected,
"line 2 must equal `{expected}`; got {commit_line:?}"
);
}
#[test]
fn version_detector_set_line_matches_core_count_and_digest() {
let (code, stdout, _e) = run(&["--version"]);
assert_eq!(code, Some(0), "--version must exit 0");
let line = stdout
.lines()
.find(|l| l.starts_with("Detector Set:"))
.expect("--version must print a `Detector Set:` line");
let count = keyhog_core::embedded_detector_count();
let digest = keyhog_core::detector_digest();
let expected = format!("Detector Set: {count} ({digest})");
assert_eq!(
line, expected,
"detector-set line must equal `{expected}`; got {line:?}"
);
let digest_prefix = format!("{count}-");
assert!(
digest.starts_with(&digest_prefix),
"digest {digest:?} must begin with the detector count `{digest_prefix}`"
);
}
#[test]
fn version_build_target_line_matches_host_arch_os() {
let (code, stdout, _e) = run(&["--version"]);
assert_eq!(code, Some(0), "--version must exit 0");
let expected = format!(
"Build Target: {}-{}",
std::env::consts::ARCH,
std::env::consts::OS
);
let line = stdout
.lines()
.find(|l| l.starts_with("Build Target:"))
.expect("--version must print a `Build Target:` line");
assert_eq!(
line, expected,
"build-target line must equal `{expected}`; got {line:?}"
);
}
#[test]
fn version_ml_model_version_line_matches_scanner_accessor() {
let (code, stdout, _e) = run(&["--version"]);
assert_eq!(code, Some(0), "--version must exit 0");
let expected = format!(
"ML Model Version: {}",
keyhog_scanner::ml_scorer::model_version()
);
let line = stdout
.lines()
.find(|l| l.starts_with("ML Model Version:"))
.expect("--version must print an `ML Model Version:` line");
assert_eq!(
line, expected,
"ml-model-version line must equal `{expected}`; got {line:?}"
);
}
#[test]
fn plain_version_prints_exactly_six_lines() {
let (code, stdout, _e) = run(&["--version"]);
assert_eq!(code, Some(0), "--version must exit 0");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(
lines.len(),
6,
"plain --version must print exactly 6 lines; got {}:\n{stdout}",
lines.len()
);
let prefixes = [
"KeyHog v",
"Commit: ",
"Detector Set: ",
"Build Target: ",
"ML Model Version: ",
"ML Model Card: ",
];
for (idx, prefix) in prefixes.iter().enumerate() {
assert!(
lines[idx].starts_with(prefix),
"line {idx} must start with {prefix:?}; got {:?}",
lines[idx]
);
}
}
#[test]
fn full_version_prefix_matches_plain_and_adds_hardware_lines() {
let (code_plain, out_plain, _e1) = run(&["--version"]);
let (code_full, out_full, _e2) = run(&["--version", "--full"]);
assert_eq!(code_plain, Some(0), "--version must exit 0");
assert_eq!(code_full, Some(0), "--version --full must exit 0");
let plain_lines: Vec<&str> = out_plain.lines().collect();
let full_lines: Vec<&str> = out_full.lines().collect();
assert_eq!(plain_lines.len(), 6, "plain --version must be 6 lines");
assert!(
full_lines.len() > 6,
"--full must append hardware lines beyond the 6 core lines; got {}",
full_lines.len()
);
assert_eq!(
&full_lines[..6],
&plain_lines[..],
"--full first 6 lines must be byte-identical to plain --version"
);
assert!(
out_full.contains("GPU Acceleration:"),
"--full must print a `GPU Acceleration:` line; got:\n{out_full}"
);
assert!(
out_full.contains("SIMD Regex:"),
"--full must print a `SIMD Regex:` line; got:\n{out_full}"
);
}
#[test]
fn version_output_is_deterministic_across_runs() {
let (_, out_a, _) = run(&["--version"]);
let (_, out_b, _) = run(&["--version"]);
assert_eq!(
out_a, out_b,
"two --version runs must produce byte-identical stdout"
);
}
#[test]
fn version_flag_short_circuits_subcommand() {
let (code, stdout, stderr) = run(&["detectors", "--version"]);
assert_eq!(
code,
Some(0),
"`detectors --version` must exit 0 via the version fast-path; stderr={stderr}"
);
let first = stdout.lines().next().expect("first line");
let expected = format!("KeyHog v{}", env!("CARGO_PKG_VERSION"));
assert_eq!(
first, expected,
"`detectors --version` must print the version banner, not detector output"
);
}
#[test]
fn top_level_help_usage_names_binary_keyhog() {
let (code, stdout, stderr) = run(&["--help"]);
assert_eq!(code, Some(0), "--help must exit 0; stderr={stderr}");
let tokens = usage_tokens(&stdout);
assert_eq!(tokens[0], "Usage:", "usage line must start with `Usage:`");
assert_eq!(
tokens[1], "keyhog",
"top-level usage must name the binary `keyhog`; usage tokens: {tokens:?}"
);
assert!(
stdout.contains("KeyHog: The developer-first secret scanner."),
"--help must carry the about tagline; got:\n{stdout}"
);
}
#[test]
fn long_help_and_short_h_are_byte_identical() {
let (code_long, out_long, _e1) = run(&["--help"]);
let (code_short, out_short, _e2) = run(&["-h"]);
assert_eq!(code_long, Some(0), "--help must exit 0");
assert_eq!(code_short, Some(0), "-h must exit 0");
assert_eq!(
out_short, out_long,
"-h and --help must print byte-identical stdout"
);
}
#[test]
fn subcommand_help_usage_agrees_on_binary_and_subcommand_name() {
let subcommands = [
"scan",
"config",
"detectors",
"explain",
"diff",
"backend",
"doctor",
"scan-system",
"calibrate-autoroute",
];
for sub in subcommands {
let (code, stdout, stderr) = run(&[sub, "--help"]);
assert_eq!(code, Some(0), "`{sub} --help` must exit 0; stderr={stderr}");
let tokens = usage_tokens(&stdout);
assert_eq!(
tokens[1], "keyhog",
"`{sub} --help` usage must name binary `keyhog`; tokens: {tokens:?}"
);
assert_eq!(
tokens[2], sub,
"`{sub} --help` usage second token must be the subcommand `{sub}`; tokens: {tokens:?}"
);
}
}
#[test]
fn unknown_subcommand_errors_exit_two_and_names_keyhog() {
let (code, stdout, stderr) = run(&["definitely-not-a-subcommand"]);
assert_eq!(
code,
Some(2),
"unknown subcommand must exit 2 (user error); stdout={stdout} stderr={stderr}"
);
assert!(
stderr.contains("Usage: keyhog"),
"clap error must surface a `Usage: keyhog` line on stderr; got:\n{stderr}"
);
}