use clap::Parser;
use keyhog::args::ScanArgs;
use keyhog::testing::{CliTestApi as _, API};
use keyhog_core::{MatchLocation, RawMatch, Severity};
use keyhog_profile::{Stage, StageMeasurement};
use sha2::Digest as _;
use std::path::Path;
use std::sync::Arc;
fn measure(f: impl FnOnce()) -> Vec<StageMeasurement> {
keyhog_profile::reset();
let runtime = keyhog_profile::Runtime::new();
let guard = runtime.enter();
f();
let measurements = keyhog_profile::take_stage_measurements();
drop(guard);
keyhog_profile::reset();
measurements
}
fn stage_calls(measurements: &[StageMeasurement], stage: Stage) -> u64 {
measurements
.iter()
.filter(|measurement| measurement.stage == stage)
.map(|measurement| measurement.calls)
.sum()
}
fn private_tempdir() -> tempfile::TempDir {
let dir = tempfile::TempDir::new().expect("tempdir");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o700))
.expect("chmod tempdir");
}
dir
}
fn raw_match(detector_id: &str) -> RawMatch {
RawMatch {
detector_id: Arc::from(detector_id),
detector_name: Arc::from("Test Detector"),
service: Arc::from("test"),
severity: Severity::High,
credential: keyhog_core::SensitiveString::from("secret"),
credential_hash: [0u8; 32].into(),
companions: std::collections::HashMap::new(),
location: MatchLocation {
source: Arc::from("filesystem"),
file_path: Some(Arc::from("demo.txt")),
line: Some(1),
offset: 0,
commit: None,
author: None,
date: None,
},
entropy: None,
confidence: None,
}
}
fn write_detector_policy_config(dir: &Path) {
std::fs::write(
dir.join(".keyhog.toml"),
"[detector.demo-detector]\nenabled = false\n",
)
.expect("write config fixture");
}
#[test]
fn args_try_parse_from_records_one_preprocess_span() {
let measurements = measure(|| {
keyhog::args::try_parse_from(["keyhog", "scan", ".", "--no-config"])
.expect("parse must succeed");
});
assert_eq!(
measurements.len(),
1,
"parse must record exactly one stage, got {measurements:?}"
);
assert_eq!(stage_calls(&measurements, Stage::Preprocess), 1);
}
#[test]
fn config_merge_records_preprocess_and_detector_policy_backend_select() {
let dir = tempfile::TempDir::new().expect("tempdir");
write_detector_policy_config(dir.path());
let scan_path = dir.path().to_string_lossy().to_string();
let mut args =
ScanArgs::try_parse_from(["scan", "--path", scan_path.as_str()]).expect("parse scan args");
let measurements = measure(|| {
API.apply_config_file_quiet(&mut args);
});
assert_eq!(
measurements.len(),
2,
"config merge must record Preprocess and BackendSelect only, got {measurements:?}"
);
assert_eq!(stage_calls(&measurements, Stage::Preprocess), 5);
assert_eq!(stage_calls(&measurements, Stage::BackendSelect), 1);
}
#[test]
fn allowlist_rule_suppressor_load_records_one_preprocess_span() {
let dir = tempfile::TempDir::new().expect("tempdir");
std::fs::write(
dir.path().join(".keyhogignore.toml"),
"[[suppress]]\ndetector = \"demo\"\n",
)
.expect("write suppressor fixture");
let measurements = measure(|| {
keyhog::profiling_test_seams::load_rule_suppressor(Some(dir.path()))
.expect("suppressor must load");
});
assert_eq!(
measurements.len(),
1,
"suppressor load must record exactly one stage, got {measurements:?}"
);
assert_eq!(stage_calls(&measurements, Stage::Preprocess), 1);
}
#[test]
fn allowlist_eval_records_one_suppression_span_and_filters() {
let suppressor: keyhog_core::RuleSuppressor = "[[suppress]]\ndetector = \"demo\"\n"
.parse()
.expect("suppressor fixture parses");
let mut kept_len = usize::MAX;
let measurements = measure(|| {
let kept = keyhog::profiling_test_seams::filter_rule_suppressed(
&suppressor,
vec![raw_match("demo")],
);
kept_len = kept.len();
});
assert_eq!(kept_len, 0, "matching rule must suppress the finding");
assert_eq!(
measurements.len(),
1,
"suppression eval must record exactly one stage, got {measurements:?}"
);
assert_eq!(stage_calls(&measurements, Stage::Suppression), 1);
}
#[test]
fn detector_corpus_load_records_one_backend_select_span() {
let mut loaded = 0usize;
let measurements = measure(|| {
loaded = keyhog::profiling_test_seams::load_detector_corpus(Path::new("detectors"))
.expect("embedded corpus must load")
.len();
});
assert!(loaded > 0, "embedded corpus must be non-empty");
assert_eq!(
measurements.len(),
1,
"detector load must record exactly one stage, got {measurements:?}"
);
assert_eq!(stage_calls(&measurements, Stage::BackendSelect), 1);
}
#[test]
fn install_asset_selection_records_one_preprocess_span() {
let mut selected = String::new();
let measurements = measure(|| {
selected = API
.select_release_asset_name("v0.5.50", &["keyhog-linux-x86_64"])
.expect("host asset must resolve");
});
assert_eq!(selected, "keyhog-linux-x86_64");
assert_eq!(stage_calls(&measurements, Stage::Preprocess), 1);
assert_eq!(stage_calls(&measurements, Stage::SourceAcquire), 0);
}
#[test]
fn release_checksum_verification_records_one_preprocess_span() {
use sha2::Digest as _;
let data = b"payload-bytes";
let asset = "keyhog-linux-x86_64";
let digest = keyhog_core::hex_encode(&sha2::Sha256::digest(data));
let checksum_file = format!("{digest} {asset}");
let measurements = measure(|| {
API.verify_release_checksum(data, asset, checksum_file.as_bytes())
.expect("checksum must verify");
});
assert_eq!(
measurements.len(),
1,
"checksum verify must record exactly one stage, got {measurements:?}"
);
assert_eq!(stage_calls(&measurements, Stage::Preprocess), 1);
}
#[test]
fn install_with_rollback_records_one_reporting_span() {
let dir = private_tempdir();
let exe = dir.path().join("keyhog");
let measurements = measure(|| {
API.install_with_rollback(&exe, b"candidate-bytes", |_| true)
.expect("install with passing health gate must succeed");
});
assert!(exe.exists(), "installed binary must exist after publish");
assert_eq!(
measurements.len(),
1,
"install must record exactly one stage, got {measurements:?}"
);
assert_eq!(stage_calls(&measurements, Stage::Reporting), 1);
}
#[test]
fn doctor_host_probe_records_one_preprocess_span() {
let measurements = measure(|| {
let caps = keyhog::profiling_test_seams::doctor_host_probe();
assert!(caps.logical_cores > 0, "probe must report host cores");
});
assert_eq!(
measurements.len(),
1,
"doctor probe must record exactly one stage, got {measurements:?}"
);
assert_eq!(stage_calls(&measurements, Stage::Preprocess), 1);
}
#[test]
fn paths_are_silent_without_runtime() {
keyhog_profile::reset();
keyhog::args::try_parse_from(["keyhog", "scan", ".", "--no-config"])
.expect("parse must succeed");
let config_dir = tempfile::TempDir::new().expect("tempdir");
write_detector_policy_config(config_dir.path());
let scan_path = config_dir.path().to_string_lossy().to_string();
let mut args =
ScanArgs::try_parse_from(["scan", "--path", scan_path.as_str()]).expect("parse scan args");
API.apply_config_file_quiet(&mut args);
let suppressor_dir = tempfile::TempDir::new().expect("tempdir");
std::fs::write(
suppressor_dir.path().join(".keyhogignore.toml"),
"[[suppress]]\ndetector = \"demo\"\n",
)
.expect("write suppressor fixture");
let suppressor =
keyhog::profiling_test_seams::load_rule_suppressor(Some(suppressor_dir.path()))
.expect("suppressor must load");
let kept =
keyhog::profiling_test_seams::filter_rule_suppressed(&suppressor, vec![raw_match("demo")]);
assert!(kept.is_empty());
keyhog::profiling_test_seams::load_detector_corpus(Path::new("detectors"))
.expect("embedded corpus must load");
API.select_release_asset_name("v0.5.50", &["keyhog-linux-x86_64"])
.expect("host asset must resolve");
let digest = keyhog_core::hex_encode(&sha2::Sha256::digest(b"payload-bytes"));
API.verify_release_checksum(
b"payload-bytes",
"keyhog-linux-x86_64",
format!("{digest} keyhog-linux-x86_64").as_bytes(),
)
.expect("checksum must verify");
let install_dir = private_tempdir();
API.install_with_rollback(
&install_dir.path().join("keyhog"),
b"candidate-bytes",
|_| true,
)
.expect("install must succeed");
keyhog::profiling_test_seams::doctor_host_probe();
let measurements = keyhog_profile::take_stage_measurements();
keyhog_profile::reset();
assert_eq!(stage_calls(&measurements, Stage::Preprocess), 0);
assert_eq!(stage_calls(&measurements, Stage::BackendSelect), 0);
assert_eq!(stage_calls(&measurements, Stage::Suppression), 0);
assert_eq!(stage_calls(&measurements, Stage::Reporting), 0);
assert_eq!(stage_calls(&measurements, Stage::SourceAcquire), 0);
assert_eq!(stage_calls(&measurements, Stage::IncrementalLookup), 0);
}