mod common;
use std::path::{Path, PathBuf};
use common::{rsigma, temp_file};
use predicates::prelude::*;
const REPORT_GOLDEN: &str = include_str!("golden/hygiene_report.json");
fn fixtures() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/hygiene")
}
fn fixture(name: &str) -> String {
fixtures().join(name).to_string_lossy().into_owned()
}
#[test]
fn hygiene_full_inputs_matches_golden() {
let output = rsigma()
.args([
"rule",
"hygiene",
"-r",
&fixture("rules.yml"),
"--metrics",
&fixture("metrics.txt"),
"--fields",
&fixture("fields.json"),
"--output-format",
"json",
])
.assert()
.success()
.get_output()
.stdout
.clone();
let actual: serde_json::Value = serde_json::from_slice(&output).expect("stdout is valid JSON");
let expected: serde_json::Value =
serde_json::from_str(REPORT_GOLDEN).expect("golden is valid JSON");
assert_eq!(actual, expected, "hygiene document drifted from golden");
}
#[test]
fn hygiene_static_signals_need_only_rules() {
let output = rsigma()
.args([
"rule",
"hygiene",
"-r",
&fixture("rules.yml"),
"--output-format",
"json",
])
.assert()
.success()
.get_output()
.stdout
.clone();
let doc: serde_json::Value = serde_json::from_slice(&output).unwrap();
assert_eq!(doc["summary"]["never_fired"], 0);
assert_eq!(doc["summary"]["noisy"], 0);
assert_eq!(doc["summary"]["broken_coverage"], 0);
assert_eq!(doc["summary"]["metrics_source"], false);
assert_eq!(doc["untagged"][0], "Delta Untagged Orphan");
assert_eq!(doc["stale_status"][0], "Foxtrot Deprecated");
}
#[test]
fn hygiene_corpus_replay_is_offline_fire_source() {
let output = rsigma()
.args([
"rule",
"hygiene",
"-r",
&fixture("rules.yml"),
"--corpus",
&fixture("corpus"),
"--output-format",
"json",
])
.assert()
.success()
.get_output()
.stdout
.clone();
let doc: serde_json::Value = serde_json::from_slice(&output).unwrap();
assert_eq!(doc["summary"]["metrics_source"], true);
let silent: Vec<&str> = doc["never_fired"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
assert!(silent.contains(&"Charlie Quiet"));
assert!(!silent.contains(&"Alpha Clean"));
assert!(!silent.contains(&"Bravo Noisy"));
}
#[test]
fn hygiene_corpus_missing_path_is_config_error() {
rsigma()
.args([
"rule",
"hygiene",
"-r",
&fixture("rules.yml"),
"--corpus",
"/no/such/corpus",
])
.assert()
.code(3)
.stderr(predicate::str::contains("corpus path not found"));
}
#[test]
fn hygiene_empty_corpus_dir_is_config_error() {
let dir = tempfile::tempdir().unwrap();
rsigma()
.args([
"rule",
"hygiene",
"-r",
&fixture("rules.yml"),
"--corpus",
dir.path().to_str().unwrap(),
])
.assert()
.code(3)
.stderr(predicate::str::contains("no corpus files found"));
}
#[test]
fn hygiene_fail_on_silent_exits_one() {
rsigma()
.args([
"rule",
"hygiene",
"-r",
&fixture("rules.yml"),
"--metrics",
&fixture("metrics.txt"),
"--fail-on",
"silent",
"--output-format",
"table",
])
.assert()
.code(1)
.stderr(predicate::str::contains("--fail-on policy matched"));
}
#[test]
fn hygiene_fail_on_unmatched_condition_is_clean_exit() {
rsigma()
.args([
"rule",
"hygiene",
"-r",
&fixture("rules.yml"),
"--metrics",
&fixture("metrics.txt"),
"--fail-on",
"broken-fields",
])
.assert()
.success();
}
#[test]
fn hygiene_default_no_fail_on_is_clean_exit() {
rsigma()
.args([
"rule",
"hygiene",
"-r",
&fixture("rules.yml"),
"--metrics",
&fixture("metrics.txt"),
"--output-format",
"table",
])
.assert()
.success();
}
#[test]
fn hygiene_report_file_is_written() {
let report = tempfile::Builder::new().suffix(".json").tempfile().unwrap();
rsigma()
.args([
"rule",
"hygiene",
"-r",
&fixture("rules.yml"),
"--metrics",
&fixture("metrics.txt"),
"--fields",
&fixture("fields.json"),
"--report",
report.path().to_str().unwrap(),
"--output-format",
"table",
])
.assert()
.success();
let written: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(report.path()).unwrap())
.expect("report file is valid JSON");
let expected: serde_json::Value = serde_json::from_str(REPORT_GOLDEN).unwrap();
assert_eq!(written, expected, "report file drifted from golden");
}
#[test]
fn hygiene_csv_emits_flagged_rows() {
rsigma()
.args([
"rule",
"hygiene",
"-r",
&fixture("rules.yml"),
"--metrics",
&fixture("metrics.txt"),
"--output-format",
"csv",
])
.assert()
.success()
.stdout(predicate::str::contains("RULE,KIND,SIGNALS"))
.stdout(predicate::str::contains("Bravo Noisy,detection,noisy"));
}
#[test]
fn hygiene_invalid_fail_on_is_config_error() {
rsigma()
.args([
"rule",
"hygiene",
"-r",
&fixture("rules.yml"),
"--fail-on",
"bogus",
])
.assert()
.code(3)
.stderr(predicate::str::contains("invalid --fail-on"));
}
#[test]
fn hygiene_invalid_silent_threshold_is_config_error() {
rsigma()
.args([
"rule",
"hygiene",
"-r",
&fixture("rules.yml"),
"--silent-threshold",
"soon",
])
.assert()
.code(3)
.stderr(predicate::str::contains("invalid --silent-threshold"));
}
#[test]
fn hygiene_no_rules_is_config_error() {
rsigma()
.args(["rule", "hygiene"])
.assert()
.code(3)
.stderr(predicate::str::contains("no rules path"));
}
#[test]
fn hygiene_unreadable_metrics_is_config_error() {
rsigma()
.args([
"rule",
"hygiene",
"-r",
&fixture("rules.yml"),
"--metrics",
"/no/such/metrics.txt",
])
.assert()
.code(3)
.stderr(predicate::str::contains("could not read metrics"));
}
#[test]
fn hygiene_config_file_layering() {
let cfg = temp_file(
".yaml",
&format!(
"hygiene:\n rules:\n - {}\n metrics: {}\n fail_on:\n - silent\n",
fixture("rules.yml"),
fixture("metrics.txt"),
),
);
rsigma()
.args([
"rule",
"hygiene",
"--config",
cfg.path().to_str().unwrap(),
"--output-format",
"table",
])
.assert()
.code(1);
}
#[test]
fn hygiene_dry_run_prints_config_section() {
rsigma()
.args(["rule", "hygiene", "-r", &fixture("rules.yml"), "--dry-run"])
.assert()
.success()
.stdout(predicate::str::contains("silent_threshold"));
}