use std::path::PathBuf;
use std::process::Command;
fn binary() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_keyhog"))
}
#[test]
fn explain_valid_detector_returns_exit_zero_with_spec() {
let output = Command::new(binary())
.arg("explain")
.arg("aws-access-key")
.output()
.expect("spawn keyhog explain aws-access-key");
assert_eq!(
output.status.code(),
Some(0),
"explain aws-access-key should exit 0; stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("aws-access-key") || stdout.contains("AWS Access Key"),
"explain output must include the detector id or name; got: {stdout}"
);
assert!(
stdout.contains("severity") || stdout.contains("regex") || stdout.contains("pattern"),
"explain output must include detector spec (severity/regex); got: {stdout}"
);
}
#[test]
fn explain_github_fine_grained_pat_detector_includes_rotation_guide() {
let output = Command::new(binary())
.arg("explain")
.arg("github-pat-fine-grained")
.output()
.expect("spawn keyhog explain github-pat-fine-grained");
assert_eq!(
output.status.code(),
Some(0),
"explain github-pat-fine-grained should exit 0"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("github") || stdout.contains("GitHub"),
"explain github-pat-fine-grained must mention github; got: {stdout}"
);
assert!(
stdout.to_lowercase().contains("revoke")
|| stdout.to_lowercase().contains("rotate")
|| stdout.to_lowercase().contains("github"),
"explain must include rotation guidance or service info; got: {stdout}"
);
}
#[test]
fn explain_invalid_detector_id_exits_two_with_actionable_error() {
let output = Command::new(binary())
.arg("explain")
.arg("detector-does-not-exist-xyz")
.output()
.expect("spawn keyhog explain <invalid>");
assert_eq!(
output.status.code(),
Some(2),
"explain with invalid detector ID should exit 2 (user error)"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.to_lowercase().contains("not found")
|| stderr.to_lowercase().contains("unknown")
|| stderr.contains("detector-does-not-exist"),
"error message must name the invalid detector so operator knows why; got: {stderr}"
);
}
#[test]
fn explain_help_documents_detector_id_argument() {
let output = Command::new(binary())
.arg("explain")
.arg("--help")
.output()
.expect("spawn keyhog explain --help");
assert_eq!(
output.status.code(),
Some(0),
"explain --help should exit 0"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("DETECTOR_ID") || stdout.contains("detector"),
"help must document the required detector-id argument; got: {stdout}"
);
assert!(
stdout.contains("--detectors"),
"help must mention the --detectors directory override flag; got: {stdout}"
);
}
#[test]
fn explain_marks_unproven_bigram_corpus_evidence() {
let output = Command::new(binary())
.args(["explain", "aws-access-key"])
.output()
.expect("spawn keyhog explain aws-access-key");
assert_eq!(
output.status.code(),
Some(0),
"explain should succeed; stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
for expected in [
"Bigram prefilter:",
"density:",
"slots; saturates at 39322",
"state: HEALTHY / CORPUS UNMEASURED",
"reject: UNMEASURED",
"parity: UNPROVEN",
"action: run `make -C benchmarks bloom`",
] {
assert!(
stdout.contains(expected),
"explain prefilter status is missing {expected:?}; stdout={stdout}"
);
}
}
#[test]
fn explain_compiled_plan_reports_required_relation() {
let output = Command::new(binary())
.args(["explain", "twilio-auth-token", "--compiled-plan"])
.output()
.expect("spawn compiled detector explanation");
assert_eq!(
output.status.code(),
Some(0),
"compiled explanation should succeed; stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
for expected in [
"Compiled evidence plan:",
"detector: twilio-auth-token",
"relations: 1",
"capture_group: 1",
"requirement: required",
"direction: either",
"scope: window",
"within_lines: 5",
"within_bytes: unbounded",
"value_relation: present",
] {
assert!(
stdout.contains(expected),
"compiled explanation is missing {expected:?}: {stdout}"
);
}
}
#[test]
fn explain_help_documents_compiled_plan_flag() {
let output = Command::new(binary())
.args(["explain", "--help"])
.output()
.expect("spawn keyhog explain --help");
assert_eq!(output.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("--compiled-plan")
&& stdout.contains("resolved capture")
&& stdout.contains("structural scope"),
"help must explain compiled evidence inspection: {stdout}"
);
}
#[test]
fn explain_compiled_plan_reports_detector_relation() {
let output = Command::new(binary())
.args(["explain", "notion-oauth-secret", "--compiled-plan"])
.output()
.expect("spawn cross-detector compiled explanation");
assert_eq!(
output.status.code(),
Some(0),
"compiled explanation should succeed; stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
for expected in [
"detector_relations: 1",
"target=notion-integration-token",
"kind=subsumes",
"direction=either",
"within_lines=0",
"within_bytes=0",
] {
assert!(
stdout.contains(expected),
"compiled explanation is missing {expected:?}: {stdout}"
);
}
}