mod common;
use std::path::{Path, PathBuf};
use common::{rsigma, temp_file};
use predicates::prelude::*;
const REPORT_GOLDEN: &str = include_str!("golden/scorecard_report.json");
const MARKDOWN_GOLDEN: &str = include_str!("golden/scorecard_report.md");
fn fixtures() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/scorecard")
}
fn fixture(name: &str) -> String {
fixtures().join(name).to_string_lossy().into_owned()
}
fn normalize_eol(s: &str) -> String {
s.replace("\r\n", "\n")
}
#[test]
fn scorecard_full_inputs_matches_golden() {
let output = rsigma()
.args([
"rule",
"scorecard",
"--backtest",
&fixture("backtest.json"),
"--coverage",
&fixture("coverage.json"),
"--metrics",
&fixture("metrics.txt"),
"--triage",
&fixture("triage.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, "scorecard document drifted from golden");
}
#[test]
fn scorecard_markdown_report_matches_golden() {
let report = tempfile::Builder::new().suffix(".md").tempfile().unwrap();
rsigma()
.args([
"rule",
"scorecard",
"--backtest",
&fixture("backtest.json"),
"--coverage",
&fixture("coverage.json"),
"--metrics",
&fixture("metrics.txt"),
"--triage",
&fixture("triage.json"),
"--report",
report.path().to_str().unwrap(),
])
.assert()
.success();
let actual = std::fs::read_to_string(report.path()).unwrap();
assert_eq!(
normalize_eol(&actual).trim_end(),
normalize_eol(MARKDOWN_GOLDEN).trim_end(),
"markdown report drifted from golden"
);
}
#[test]
fn scorecard_required_inputs_only_succeeds() {
rsigma()
.args([
"rule",
"scorecard",
"--backtest",
&fixture("backtest.json"),
"--coverage",
&fixture("coverage.json"),
"--output-format",
"table",
])
.assert()
.success()
.stdout(predicate::str::contains("Detection scorecard"))
.stdout(predicate::str::contains("Retire"));
}
#[test]
fn scorecard_metrics_absent_degrades_to_corpus_volume() {
let output = rsigma()
.args([
"rule",
"scorecard",
"--backtest",
&fixture("backtest.json"),
"--coverage",
&fixture("coverage.json"),
"--output-format",
"json",
])
.assert()
.success()
.get_output()
.stdout
.clone();
let doc: serde_json::Value = serde_json::from_slice(&output).unwrap();
let keep = doc["records"]
.as_array()
.unwrap()
.iter()
.find(|r| r["rule_id"] == "k1")
.unwrap();
assert_eq!(keep["volume"], 10);
assert!(keep.get("production_volume").is_none());
}
#[test]
fn scorecard_fail_on_retire_exits_one() {
rsigma()
.args([
"rule",
"scorecard",
"--backtest",
&fixture("backtest.json"),
"--coverage",
&fixture("coverage.json"),
"--fail-on",
"retire",
"--output-format",
"table",
])
.assert()
.code(1)
.stderr(predicate::str::contains(
"at or worse than --fail-on retire",
));
}
#[test]
fn scorecard_default_fail_on_none_is_clean_exit() {
rsigma()
.args([
"rule",
"scorecard",
"--backtest",
&fixture("backtest.json"),
"--coverage",
&fixture("coverage.json"),
])
.assert()
.success();
}
#[test]
fn scorecard_malformed_report_is_config_error() {
let bad = temp_file(".json", "this is not json {");
rsigma()
.args([
"rule",
"scorecard",
"--backtest",
bad.path().to_str().unwrap(),
"--coverage",
&fixture("coverage.json"),
])
.assert()
.code(3)
.stderr(predicate::str::contains("could not parse backtest report"));
}
#[test]
fn scorecard_unreadable_input_is_input_error() {
rsigma()
.args([
"rule",
"scorecard",
"--backtest",
"/no/such/backtest/report.json",
"--coverage",
&fixture("coverage.json"),
])
.assert()
.code(2)
.stderr(predicate::str::contains("could not read"));
}
#[test]
fn scorecard_missing_required_flag_is_config_error() {
rsigma()
.args(["rule", "scorecard", "--coverage", &fixture("coverage.json")])
.assert()
.code(3)
.stderr(predicate::str::contains("no backtest report"));
}
#[test]
fn scorecard_bad_report_format_is_config_error() {
let out = tempfile::Builder::new().suffix(".txt").tempfile().unwrap();
rsigma()
.args([
"rule",
"scorecard",
"--backtest",
&fixture("backtest.json"),
"--coverage",
&fixture("coverage.json"),
"--report",
out.path().to_str().unwrap(),
])
.assert()
.code(3)
.stderr(predicate::str::contains("cannot determine report format"));
}
#[test]
fn scorecard_config_file_layering() {
let cfg = temp_file(".yaml", "scorecard:\n fail_on: retire\n");
rsigma()
.args([
"rule",
"scorecard",
"--backtest",
&fixture("backtest.json"),
"--coverage",
&fixture("coverage.json"),
"--config",
cfg.path().to_str().unwrap(),
"--output-format",
"table",
])
.assert()
.code(1);
}
#[test]
fn scorecard_reads_reports_from_config() {
let cfg = temp_file(
".yaml",
&format!(
"scorecard:\n backtest: {}\n coverage: {}\n",
fixture("backtest.json"),
fixture("coverage.json"),
),
);
rsigma()
.args([
"rule",
"scorecard",
"--config",
cfg.path().to_str().unwrap(),
"--output-format",
"table",
])
.assert()
.success()
.stdout(predicate::str::contains("Detection scorecard"));
}
#[test]
fn scorecard_dry_run_prints_config_section() {
rsigma()
.args([
"rule",
"scorecard",
"--backtest",
&fixture("backtest.json"),
"--coverage",
&fixture("coverage.json"),
"--dry-run",
])
.assert()
.success()
.stdout(predicate::str::contains("min_precision"));
}