use std::path::PathBuf;
use std::process::Command;
fn bin() -> &'static str {
env!("CARGO_BIN_EXE_candor-query")
}
struct Fixture {
dir: PathBuf,
prefix: String,
}
impl Fixture {
fn new(name: &str) -> Self {
let dir = std::env::temp_dir().join(format!("candor-query-cli-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let prefix = dir.join("r").to_string_lossy().into_owned();
Fixture { dir, prefix }
}
fn write_report(&self) {
let report = r#"{
"candor": { "version": "scan-test", "toolchain": "stable", "spec": "0.7" },
"package": "rpt",
"functions": [
{ "fn": "inner", "loc": "src/lib.rs:2:1", "inferred": ["Fs"], "direct": ["Fs"], "hash": "rpt#inner", "paths": ["/x"] },
{ "fn": "outer", "loc": "src/lib.rs:1:1", "inferred": ["Fs"], "hash": "rpt#outer", "paths": ["/x"], "calls": ["inner"] }
]
}"#;
std::fs::write(format!("{}.rpt.scan.json", self.prefix), report).unwrap();
std::fs::write(format!("{}.rpt.scan.callgraph.json", self.prefix), r#"{"inner":[],"outer":["inner"]}"#).unwrap();
}
fn report_path(&self) -> String {
format!("{}.rpt.scan.json", self.prefix)
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.dir);
}
}
#[test]
fn version_exits_0() {
for flag in ["--version", "-V"] {
let out = Command::new(bin()).arg(flag).output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0), "{flag} must exit 0");
let stdout = String::from_utf8(out.stdout).expect("utf8 stdout");
assert!(stdout.lines().next().unwrap_or("").starts_with("candor-query "),
"{flag} must print the build banner, got:\n{stdout}");
}
}
#[test]
fn help_exits_0() {
for flag in ["--help", "-h"] {
let out = Command::new(bin()).arg(flag).output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0), "{flag} must exit 0");
let stdout = String::from_utf8(out.stdout).expect("utf8 stdout");
assert!(stdout.contains("USAGE"), "{flag} must print a USAGE line, got:\n{stdout}");
}
}
#[test]
fn unknown_command_exits_2() {
let out = Command::new(bin()).arg("boguscmd").output().expect("run candor-query");
assert_eq!(out.status.code(), Some(2), "an unknown command must exit 2");
let stderr = String::from_utf8(out.stderr).expect("utf8 stderr");
assert!(stderr.contains("unknown command"), "must report `unknown command`, got:\n{stderr}");
}
#[test]
fn no_command_exits_2() {
let out = Command::new(bin()).output().expect("run candor-query");
assert_eq!(out.status.code(), Some(2), "no command must exit 2");
}
#[test]
fn corrupt_report_does_not_panic() {
let f = Fixture::new("corrupt");
std::fs::write(f.report_path(), "{ this is : not valid json @@@").unwrap();
let out = Command::new(bin()).arg("map").arg(&f.prefix).arg("0").output().expect("run candor-query");
assert_ne!(out.status.code(), Some(101), "a corrupt report must not panic the query");
let stderr = String::from_utf8(out.stderr).expect("utf8 stderr");
assert!(stderr.contains("failed to parse"), "a corrupt report must be disclosed on stderr, got:\n{stderr}");
}
#[test]
fn truncated_report_does_not_panic() {
let f = Fixture::new("truncated");
std::fs::write(f.report_path(), r#"{"candor":{"version":"scan-test","spec":"0.7","toolch"#).unwrap();
let out = Command::new(bin()).arg("map").arg(&f.prefix).arg("0").output().expect("run candor-query");
assert_ne!(out.status.code(), Some(101), "a truncated report must not panic the query");
let stderr = String::from_utf8(out.stderr).expect("utf8 stderr");
assert!(stderr.contains("failed to parse"), "a truncated report must be disclosed on stderr, got:\n{stderr}");
}
fn write_two_segment_sidecars(prefix: &str) {
std::fs::write(format!("{prefix}.app.hierarchy.json"), r#"{"app.Sub":["app.Base"]}"#).unwrap();
std::fs::write(format!("{prefix}.app.callgraph.json"), r#"{"outer":["inner"],"inner":[]}"#).unwrap();
}
#[test]
fn a_wellformed_sidecar_is_never_diagnosed_as_a_corrupt_report() {
let f = Fixture::new("sidecar-quiet");
f.write_report();
write_two_segment_sidecars(&f.prefix);
for args in [
vec!["callers", &f.prefix, "inner", "1", "--include-unknown"],
vec!["map", &f.prefix, "0"],
vec!["where", "Fs", "--report", &f.prefix],
vec!["show", "outer", "--report", &f.prefix],
vec!["path", "outer", "Fs", "--report", &f.prefix],
vec!["tour", "--report", &f.prefix],
vec!["reachable", "--report", &f.prefix],
vec!["containment", "--report", &f.prefix],
vec!["blindspots", "--report", &f.prefix],
vec!["impact", "inner", "--report", &f.prefix],
] {
let out = Command::new(bin()).args(&args).output().expect("run candor-query");
let stderr = String::from_utf8(out.stderr).expect("utf8 stderr");
assert!(
!stderr.contains("failed to parse"),
"`{}` diagnosed a well-formed SIDECAR as a corrupt report — a false disclosure over a good \
scan:\n{stderr}",
args.join(" ")
);
}
let out = Command::new(bin()).arg("reports").arg(&f.prefix).output().expect("run candor-query");
let stdout = String::from_utf8(out.stdout).expect("utf8 stdout");
assert!(stdout.contains("r.rpt.scan.json"), "the real report must be listed, got:\n{stdout}");
assert!(!stdout.contains("hierarchy") && !stdout.contains(".app.callgraph"),
"a sidecar was listed as a report, got:\n{stdout}");
let g = Fixture::new("sidecar-empty");
std::fs::write(
format!("{}.rpt.scan.json", g.prefix),
r#"{"candor":{"version":"scan-test","toolchain":"stable","spec":"0.7"},"package":"rpt","functions":[]}"#,
).unwrap();
write_two_segment_sidecars(&g.prefix);
let out = Command::new(bin()).arg("where").arg("Fs").arg("--report").arg(&g.prefix)
.output().expect("run candor-query");
let stderr = String::from_utf8(out.stderr).expect("utf8 stderr");
assert_eq!(out.status.code(), Some(0),
"an effect-free crate beside a sidecar must still answer, not be refused as corrupt:\n{stderr}");
assert!(!stderr.contains("refusing to report an empty"),
"the corruption guard fired on a sidecar, not on a corrupt report:\n{stderr}");
}
#[test]
fn a_corrupt_report_beside_a_sidecar_is_still_disclosed() {
let f = Fixture::new("sidecar-control");
std::fs::write(f.report_path(), "{ this is : not valid json @@@").unwrap();
write_two_segment_sidecars(&f.prefix);
let out = Command::new(bin()).arg("map").arg(&f.prefix).arg("0").output().expect("run candor-query");
let stderr = String::from_utf8(out.stderr).expect("utf8 stderr");
assert!(stderr.contains("failed to parse"),
"a REAL corrupt report went undisclosed — the sidecar exclusion disabled the channel \
instead of un-poisoning it:\n{stderr}");
assert!(stderr.contains("r.rpt.scan.json"),
"the disclosure must name the corrupt REPORT, got:\n{stderr}");
assert!(!stderr.contains("hierarchy.json"),
"the disclosure named a SIDECAR, got:\n{stderr}");
assert_eq!(out.status.code(), Some(2),
"a wholly corrupt report must still fail loud, got:\n{stderr}");
}
#[test]
fn nonexistent_prefix_fails_loud_exit_2() {
let f = Fixture::new("nopfx"); let out = Command::new(bin()).arg("map").arg(&f.prefix).arg("0").output().expect("run candor-query");
assert_eq!(out.status.code(), Some(2), "a prefix with no reports must exit 2");
let stderr = String::from_utf8(out.stderr).expect("utf8 stderr");
assert!(stderr.contains("no report files"), "must report `no report files`, got:\n{stderr}");
}
#[test]
fn parsepolicy_unreadable_exits_2() {
let f = Fixture::new("ppol");
let missing = f.dir.join("does-not-exist.policy");
let out = Command::new(bin())
.arg("parsepolicy")
.arg(missing.to_string_lossy().as_ref())
.output()
.expect("run candor-query");
assert_eq!(out.status.code(), Some(2), "parsepolicy over an unreadable file must exit 2");
let stderr = String::from_utf8(out.stderr).expect("utf8 stderr");
assert!(stderr.contains("cannot read policy"), "must report the read failure, got:\n{stderr}");
}
#[test]
fn parsepolicy_missing_arg_exits_2() {
let out = Command::new(bin()).arg("parsepolicy").output().expect("run candor-query");
assert_eq!(out.status.code(), Some(2), "parsepolicy with no path must exit 2 (usage)");
}
#[test]
fn parsepolicy_valid_dumps_json_exit_0() {
let f = Fixture::new("ppolok");
let pp = f.dir.join("p.policy");
std::fs::write(&pp, "deny Net Db domain\nallow Net in billing api.stripe.com\n").unwrap();
let out = Command::new(bin())
.arg("parsepolicy")
.arg(pp.to_string_lossy().as_ref())
.output()
.expect("run candor-query");
assert_eq!(out.status.code(), Some(0), "a valid parsepolicy must exit 0");
let stdout = String::from_utf8(out.stdout).expect("utf8 stdout");
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("parsepolicy must emit JSON");
assert!(v.get("deny").is_some() && v.get("allow").is_some() && v.get("forbid").is_some(),
"parsepolicy JSON must carry deny/allow/forbid keys, got:\n{stdout}");
}
#[test]
fn whatif_nonexistent_policy_exits_2() {
let f = Fixture::new("wfpol");
f.write_report();
let bogus = f.dir.join("typo.policy");
let out = Command::new(bin())
.arg("whatif")
.arg(&f.prefix)
.arg("inner")
.arg("Net")
.arg(bogus.to_string_lossy().as_ref())
.arg("0")
.output()
.expect("run candor-query");
assert_eq!(out.status.code(), Some(2), "whatif with an unreadable policy must exit 2, not gate-green");
let stderr = String::from_utf8(out.stderr).expect("utf8 stderr");
assert!(stderr.contains("could not be read"), "must report the policy read failure, got:\n{stderr}");
}
fn write_containment_report(dir: &std::path::Path, name: &str, web_has_db: bool) -> String {
let prefix = dir.join(name).to_string_lossy().into_owned();
let web_direct = if web_has_db { r#","direct":["Db"]"# } else { "" };
let report = format!(
r#"{{"candor":{{"version":"scan-test","toolchain":"stable","spec": "0.23"}},"package":"cnt","functions":[
{{"fn":"app::data::save","inferred":["Db"],"direct":["Db"]}},
{{"fn":"app::web::page","inferred":["Db"]{web_direct}}}
]}}"#
);
std::fs::write(format!("{prefix}.cnt.scan.json"), report).unwrap();
prefix
}
#[test]
fn containment_ratchet_violation_exits_1_and_names_the_leak() {
let f = Fixture::new("cnt-leak");
let base = write_containment_report(&f.dir, "base", false);
let cur = write_containment_report(&f.dir, "cur", true);
let out = Command::new(bin()).arg("containment").arg(&cur).arg(&base).output().expect("run");
assert_eq!(out.status.code(), Some(1), "a containment leak must exit 1 (the ratchet bites)");
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(stdout.contains("[AS-EFF-010]"), "the ratchet line carries its AS-EFF code: {stdout}");
assert!(stdout.contains("Db → web"), "the leak names effect and layer: {stdout}");
}
#[test]
fn containment_ratchet_clean_exits_0() {
let f = Fixture::new("cnt-clean");
let base = write_containment_report(&f.dir, "base", true);
let cur = write_containment_report(&f.dir, "cur", true);
let out = Command::new(bin()).arg("containment").arg(&cur).arg(&base).output().expect("run");
assert_eq!(out.status.code(), Some(0), "an unchanged containment picture must exit 0");
let cur2 = write_containment_report(&f.dir, "cur2", false);
let out = Command::new(bin()).arg("containment").arg(&cur2).arg(&base).output().expect("run");
assert_eq!(out.status.code(), Some(0), "a cleanup (layer left) must exit 0");
assert!(String::from_utf8(out.stdout).unwrap().contains("improved"),
"the cleanup is noted informationally");
}
#[test]
fn gate_verdict_absent_parts_is_the_clean_verdict() {
let f = Fixture::new("gv-clean");
let out = Command::new(bin())
.arg("gate-verdict").arg(f.dir.join("nosuch.parts").to_string_lossy().as_ref()).arg("-")
.output().expect("run");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value = serde_json::from_str(String::from_utf8(out.stdout).unwrap().trim()).unwrap();
assert_eq!(v["ok"], true);
assert_eq!(v["spec"], candor_report::SPEC_VERSION); assert_eq!(v["violations"], serde_json::json!([]));
}
#[test]
fn gate_verdict_assembles_and_sorts_records_into_a_failing_verdict() {
let f = Fixture::new("gv-viol");
let parts = f.dir.join("gate.json.parts");
std::fs::write(&parts, concat!(
r#"{"rule":"AS-EFF-009","fn":"b","effects":[],"detail":"z"}"#, "\n",
r#"{"rule":"AS-EFF-006","fn":"a","effects":["Net"],"detail":"y"}"#, "\n",
)).unwrap();
let outfile = f.dir.join("gate.json");
let out = Command::new(bin())
.arg("gate-verdict").arg(parts.to_string_lossy().as_ref()).arg(outfile.to_string_lossy().as_ref())
.output().expect("run");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&outfile).unwrap()).unwrap();
assert_eq!(v["ok"], false);
let arr = v["violations"].as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0]["rule"], "AS-EFF-006", "records sort by (rule, detail)");
assert_eq!(arr[0]["fn"], "a");
assert_eq!(arr[0]["effects"], serde_json::json!(["Net"]));
}
#[test]
fn gate_verdict_corrupt_record_fails_closed() {
let f = Fixture::new("gv-corrupt");
let parts = f.dir.join("gate.json.parts");
std::fs::write(&parts, "{not json@@\n").unwrap();
let outfile = f.dir.join("gate.json");
let out = Command::new(bin())
.arg("gate-verdict").arg(parts.to_string_lossy().as_ref()).arg(outfile.to_string_lossy().as_ref())
.output().expect("run");
assert_eq!(out.status.code(), Some(2), "a corrupt record must fail closed");
assert!(!outfile.exists(), "no partial verdict may be written");
}
#[test]
fn gate_verdict_flag_shaped_report_value_is_a_usage_error_not_a_green_verdict() {
let f = Fixture::new("gv-b13");
let out = Command::new(bin())
.arg("gate-verdict").arg(f.dir.join("nosuch.parts").to_string_lossy().as_ref()).arg("-")
.arg("--report").arg("--json")
.output().expect("run");
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
assert_eq!(out.status.code(), Some(2), "--report was given no value — a usage error:\n{stderr}");
assert!(!stdout.contains("\"ok\""), "no verdict of any colour may be emitted, got:\n{stdout}");
assert!(stderr.contains("--report") && stderr.contains("--json"),
"stderr names the flag given no value AND the token that is not one: {stderr}");
f.write_report();
let out = Command::new(bin())
.arg("gate-verdict").arg(f.dir.join("nosuch.parts").to_string_lossy().as_ref()).arg("-")
.arg("--report").arg(f.report_path())
.output().expect("run");
assert_eq!(out.status.code(), Some(0), "a value-shaped --report locator is unaffected");
let v: serde_json::Value = serde_json::from_str(String::from_utf8(out.stdout).unwrap().trim()).unwrap();
assert_eq!(v["ok"], true);
}
#[test]
fn reports_exists_and_backend_probe() {
let f = Fixture::new("rp-probe");
let out = Command::new(bin()).arg("reports").arg(&f.prefix).arg("--exists").output().expect("run");
assert_ne!(out.status.code(), Some(0), "no reports → --exists must be falsy");
f.write_report();
let out = Command::new(bin()).arg("reports").arg(&f.prefix).arg("--exists").output().expect("run");
assert_eq!(out.status.code(), Some(0), "a present report → --exists exit 0");
let out = Command::new(bin()).arg("reports").arg(&f.prefix).arg("--backend").output().expect("run");
assert_eq!(out.status.code(), Some(0));
assert_eq!(String::from_utf8(out.stdout).unwrap().trim(), "scan",
"a `.scan.json` report probes as the scan backend");
}
#[test]
fn reports_clear_other_given_no_value_is_a_usage_error_not_a_silent_no_op() {
let f = Fixture::new("rp-b13");
f.write_report();
for extra in [&["--clear-other", "--exists"][..], &["--clear-other"][..]] {
let out = Command::new(bin()).arg("reports").arg(&f.prefix).args(extra).output().expect("run");
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
assert_eq!(out.status.code(), Some(2), "{extra:?} is a usage error, never a silent no-op:\n{stderr}");
assert!(stderr.contains("--clear-other"), "the refusal names the broken flag: {stderr}");
assert!(std::path::Path::new(&f.report_path()).exists(),
"a refused clear must not have removed anything");
}
let out = Command::new(bin()).arg("reports").arg(&f.prefix).args(["--clear-other", "lint"]).output().expect("run");
assert_eq!(out.status.code(), Some(0));
assert!(!std::path::Path::new(&f.report_path()).exists(), "keep=lint clears the scan report");
}
#[test]
fn engine_version_reads_the_embedded_tag() {
let f = Fixture::new("ev-tag");
let lib = f.dir.join("libfake.so");
std::fs::write(&lib, b"\x7fELFjunk candor-build-version=abc1234 morejunk").unwrap();
let out = Command::new(bin()).arg("engine-version").arg(lib.to_string_lossy().as_ref()).output().expect("run");
assert_eq!(out.status.code(), Some(0));
assert_eq!(String::from_utf8(out.stdout).unwrap().trim(), "abc1234");
let bare = f.dir.join("bare.so");
std::fs::write(&bare, b"nothing here").unwrap();
let out = Command::new(bin()).arg("engine-version").arg(bare.to_string_lossy().as_ref()).output().expect("run");
assert_ne!(out.status.code(), Some(0), "no embedded tag → non-zero");
}
#[test]
fn diff_reports_a_gained_effect() {
let f = Fixture::new("df-gain");
let base = f.dir.join("b").to_string_lossy().into_owned();
let cur = f.dir.join("c").to_string_lossy().into_owned();
let mk = |prefix: &str, effs: &str| {
std::fs::write(format!("{prefix}.d.scan.json"), format!(
r#"{{"candor":{{"version":"scan-test","toolchain":"stable","spec": "0.23"}},"functions":[
{{"fn":"worker","inferred":[{effs}],"direct":[{effs}]}}]}}"#)).unwrap();
};
mk(&base, r#""Fs""#);
mk(&cur, r#""Fs","Net""#);
let out = Command::new(bin())
.arg("diff").arg(&cur).arg(&base).arg("1").arg("v1").arg("v1")
.output().expect("run");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value = serde_json::from_str(String::from_utf8(out.stdout).unwrap().trim()).unwrap();
let gained = v.pointer("/changed/0/gained").or_else(|| v.pointer("/gained"));
assert!(serde_json::to_string(&v).unwrap().contains("Net"),
"the +Net gain must appear in the diff JSON: {v} (gained={gained:?})");
}
#[test]
fn diff_and_rewire_reject_a_flag_they_do_not_take_loud() {
let f = Fixture::new("df-rw-flags");
f.write_report();
let rep = f.report_path();
for flag in ["--report", "--policy", "--class", "--clear-other", "--gate-json", "--polciy"] {
let d = Command::new(bin()).args(["diff", &rep, &rep, flag, "--json"]).output().expect("run");
let de = String::from_utf8_lossy(&d.stderr).into_owned();
assert_eq!(d.status.code(), Some(2), "diff {flag} --json must be a usage error, not a clean answer:\n{de}");
assert!(de.contains(flag), "diff's refusal names the broken flag `{flag}`: {de}");
let r = Command::new(bin()).args(["rewire", &f.prefix, &f.prefix, flag, "--json"]).output().expect("run");
let re = String::from_utf8_lossy(&r.stderr).into_owned();
assert_eq!(r.status.code(), Some(2), "rewire {flag} --json must be a usage error, not a clean answer:\n{re}");
assert!(re.contains(flag), "rewire's refusal names the broken flag `{flag}`: {re}");
}
let ok = Command::new(bin()).args(["diff", &rep, &rep, "--json", "--text"]).output().expect("run");
assert_eq!(ok.status.code(), Some(0), "--json/--text stay accepted: {}", String::from_utf8_lossy(&ok.stderr));
let rj = Command::new(bin()).args(["rewire", &f.prefix, &f.prefix, "--json"]).output().expect("run");
assert_eq!(rj.status.code(), Some(0));
assert!(String::from_utf8_lossy(&rj.stdout).contains("\"dropped\""),
"rewire --json emits the JSON shape, not prose");
}
#[test]
fn whatif_unknown_effect_exits_2() {
let f = Fixture::new("wfeff");
f.write_report();
let out = Command::new(bin())
.arg("whatif")
.arg(&f.prefix)
.arg("inner")
.arg("net")
.arg("0")
.output()
.expect("run candor-query");
assert_eq!(out.status.code(), Some(2), "whatif with an unknown effect must exit 2");
let stderr = String::from_utf8(out.stderr).expect("utf8 stderr");
assert!(stderr.contains("unknown effect"), "must report `unknown effect`, got:\n{stderr}");
}
#[test]
fn where_unknown_effect_exits_2() {
let f = Fixture::new("wheff");
f.write_report();
let out = Command::new(bin()).arg("where").arg("Networkxyz").arg("--report").arg(f.report_path())
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(2), "where with an unknown effect must exit 2");
assert!(String::from_utf8_lossy(&out.stderr).contains("unknown effect"),
"must report `unknown effect`, got:\n{}", String::from_utf8_lossy(&out.stderr));
let ok = Command::new(bin()).arg("where").arg("Db").arg("--report").arg(f.report_path())
.output().expect("run candor-query");
assert_eq!(ok.status.code(), Some(0), "a known-but-absent effect is a valid 0-result, not an error");
}
#[test]
fn callers_nonexistent_fn_exits_2() {
let f = Fixture::new("cleff");
f.write_report();
let out = Command::new(bin()).arg("callers").arg("zzz_no_such_fn").arg("--report").arg(f.report_path())
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(2), "callers of a nonexistent fn must exit 2");
assert!(String::from_utf8_lossy(&out.stderr).contains("no function matching"),
"must report `no function matching`, got:\n{}", String::from_utf8_lossy(&out.stderr));
let ok = Command::new(bin()).arg("callers").arg("inner").arg("--report").arg(f.report_path())
.output().expect("run candor-query");
assert_eq!(ok.status.code(), Some(0), "callers of a real fn resolves at exit 0");
}
#[test]
fn unknown_flag_exits_2_but_cross_engine_flag_tolerated() {
let f = Fixture::new("flag");
f.write_report();
let bad = Command::new(bin()).arg("where").arg("Fs").arg("--polciy").arg("/x").arg("--report").arg(f.report_path())
.output().expect("run candor-query");
assert_eq!(bad.status.code(), Some(2), "a typo'd flag must exit 2, not run green with no policy");
let se = String::from_utf8_lossy(&bad.stderr);
assert!(se.contains("unknown flag") && se.contains("--policy"),
"must name the unknown flag + suggest --policy, got:\n{se}");
let txt = Command::new(bin()).arg("where").arg("Fs").arg("--text").arg("--report").arg(f.report_path())
.output().expect("run candor-query");
assert_ne!(txt.status.code(), Some(2), "--text (a valid cross-engine flag) must be tolerated, not rejected");
}
fn write_orderflow_fixture(f: &Fixture) {
let report = r#"{
"candor": { "version": "scan-test", "toolchain": "stable", "spec": "0.23" },
"package": "of",
"functions": [
{ "fn": "api::get_quote", "loc": "src/api.rs:3:1", "inferred": ["Net"], "hash": "of#gq", "paths": ["/x"], "calls": ["domain::quote_bulk"] },
{ "fn": "domain::quote_bulk", "loc": "src/domain.rs:5:1", "inferred": ["Net"], "hash": "of#qb", "paths": ["/x"], "calls": ["domain::price_quote"] },
{ "fn": "domain::price_quote","loc": "src/domain.rs:9:1", "inferred": ["Net"], "hash": "of#pq", "paths": ["/x"], "calls": ["infra::fetch_rate"] },
{ "fn": "infra::fetch_rate", "loc": "src/infra.rs:2:1", "inferred": ["Net"], "direct": ["Net"], "hash": "of#fr", "paths": ["/x"], "calls": [] }
]
}"#;
std::fs::write(format!("{}.of.scan.json", f.prefix), report).unwrap();
}
fn write_policy(f: &Fixture, name: &str, body: &str) -> String {
let p = f.dir.join(name);
std::fs::write(&p, body).unwrap();
p.to_string_lossy().into_owned()
}
#[test]
fn fix_orderflow_hoists_net_to_api() {
let f = Fixture::new("fixof");
write_orderflow_fixture(&f);
let pol = write_policy(&f, "p.policy", "deny Net domain\n");
let out = Command::new(bin())
.args(["fix", &f.prefix, "price_quote", "Net", &pol, "1"])
.output()
.expect("run candor-query");
assert_eq!(out.status.code(), Some(0), "a computable fix must exit 0");
let v: serde_json::Value =
serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).expect("fix --json must emit JSON");
assert_eq!(v["cleanHoist"], serde_json::json!(true), "a clean hoist exists");
assert_eq!(v["layer"], serde_json::json!("domain"));
assert_eq!(v["site"], serde_json::json!(["infra::fetch_rate"]), "the direct site is the infra leaf");
assert_eq!(v["hoistTo"], serde_json::json!(["api::get_quote"]), "hoist to the nearest allowed caller");
assert_eq!(
v["deniedSpan"],
serde_json::json!(["domain::price_quote", "domain::quote_bulk"]),
"the pure span is exactly the two domain functions"
);
assert_eq!(v["policyAlternative"], serde_json::json!("allow Net domain"));
assert_eq!(v["hoistHigher"], serde_json::json!([]), "the frontier is the top; no higher hoist");
}
#[test]
fn fix_surfaces_higher_hoist_tradeoff() {
let f = Fixture::new("fixhigher");
let report = r#"{
"candor": { "version": "scan-test", "toolchain": "stable", "spec": "0.23" },
"package": "of",
"functions": [
{ "fn": "main::run", "loc": "src/main.rs:1:1", "inferred": ["Net"], "hash": "of#mr", "paths": ["/x"], "calls": ["api::get_quote"] },
{ "fn": "api::get_quote", "loc": "src/api.rs:3:1", "inferred": ["Net"], "hash": "of#gq", "paths": ["/x"], "calls": ["domain::quote_bulk"] },
{ "fn": "domain::quote_bulk", "loc": "src/domain.rs:5:1", "inferred": ["Net"], "hash": "of#qb", "paths": ["/x"], "calls": ["domain::price_quote"] },
{ "fn": "domain::price_quote","loc": "src/domain.rs:9:1", "inferred": ["Net"], "hash": "of#pq", "paths": ["/x"], "calls": ["infra::fetch_rate"] },
{ "fn": "infra::fetch_rate", "loc": "src/infra.rs:2:1", "inferred": ["Net"], "direct": ["Net"], "hash": "of#fr", "paths": ["/x"], "calls": [] }
]
}"#;
std::fs::write(format!("{}.of.scan.json", f.prefix), report).unwrap();
let pol = write_policy(&f, "p.policy", "deny Net domain\n");
let out = Command::new(bin())
.args(["fix", &f.prefix, "price_quote", "Net", &pol, "1"])
.output()
.expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert_eq!(v["hoistTo"], serde_json::json!(["api::get_quote"]), "the MINIMAL frontier is unchanged");
assert_eq!(v["hoistHigher"], serde_json::json!(["main::run"]), "main::run is the higher hoist option");
let text = Command::new(bin())
.args(["fix", &f.prefix, "price_quote", "Net", &pol, "0"])
.output()
.expect("run candor-query");
let s = String::from_utf8(text.stdout).unwrap();
assert!(s.contains("TRADE-OFF") && s.contains("main::run"), "text must surface the higher-hoist trade-off, got:\n{s}");
}
#[test]
fn fix_prefers_the_effect_performing_match() {
let f = Fixture::new("fixresolve");
let report = r#"{
"candor": { "version": "scan-test", "toolchain": "stable", "spec": "0.23" },
"package": "of",
"functions": [
{ "fn": "cache::save", "loc": "src/c.rs:1:1", "inferred": [], "hash": "of#cs", "paths": ["/x"], "calls": [] },
{ "fn": "repo::save", "loc": "src/r.rs:1:1", "inferred": ["Net"], "direct": ["Net"], "hash": "of#rs", "paths": ["/x"], "calls": [] }
]
}"#;
std::fs::write(format!("{}.of.scan.json", f.prefix), report).unwrap();
let pol = write_policy(&f, "p.policy", "deny Net repo\n");
let out = Command::new(bin())
.args(["fix", &f.prefix, "save", "Net", &pol, "1"])
.output()
.expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert_eq!(v["fn"], serde_json::json!("repo::save"), "must resolve to the effectful, denied match");
assert_eq!(v["site"], serde_json::json!(["repo::save"]), "repo::save is the direct site");
}
#[test]
fn fix_sandwiched_layer_is_not_a_clean_hoist() {
let f = Fixture::new("fixsandwich");
let report = r#"{
"candor": { "version": "scan-test", "toolchain": "stable", "spec": "0.23" },
"package": "of",
"functions": [
{ "fn": "domain::top", "loc": "src/d.rs:1:1", "inferred": ["Net"], "hash": "of#t", "paths": ["/x"], "calls": ["api::mid"] },
{ "fn": "api::mid", "loc": "src/a.rs:1:1", "inferred": ["Net"], "hash": "of#m", "paths": ["/x"], "calls": ["domain::inner"] },
{ "fn": "domain::inner", "loc": "src/d.rs:9:1", "inferred": ["Net"], "hash": "of#i", "paths": ["/x"], "calls": ["infra::fetch"] },
{ "fn": "infra::fetch", "loc": "src/i.rs:1:1", "inferred": ["Net"], "direct": ["Net"], "hash": "of#f", "paths": ["/x"], "calls": [] }
]
}"#;
std::fs::write(format!("{}.of.scan.json", f.prefix), report).unwrap();
let pol = write_policy(&f, "p.policy", "deny Net domain\n");
let out = Command::new(bin())
.args(["fix", &f.prefix, "inner", "Net", &pol, "1"])
.output()
.expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert_eq!(v["cleanHoist"], serde_json::json!(false), "a sandwiched frontier is NOT a clean hoist");
let text = Command::new(bin())
.args(["fix", &f.prefix, "inner", "Net", &pol, "0"])
.output()
.expect("run candor-query");
let s = String::from_utf8(text.stdout).unwrap();
assert!(s.contains("CALLED BY") && s.contains("sandwich"), "text must explain the sandwich, got:\n{s}");
}
#[test]
fn fix_no_clean_hoist_offers_port_and_policy() {
let f = Fixture::new("fixnc");
let report = r#"{
"candor": { "version": "scan-test", "toolchain": "stable", "spec": "0.23" },
"package": "nc",
"functions": [
{ "fn": "domain::main_flow", "loc": "src/d.rs:1:1", "inferred": ["Net"], "hash": "nc#mf", "paths": ["/x"], "calls": ["domain::price_quote"] },
{ "fn": "domain::price_quote", "loc": "src/d.rs:9:1", "inferred": ["Net"], "direct": ["Net"], "hash": "nc#pq", "paths": ["/x"], "calls": [] }
]
}"#;
std::fs::write(format!("{}.nc.scan.json", f.prefix), report).unwrap();
let pol = write_policy(&f, "p.policy", "deny Net domain\n");
let out = Command::new(bin())
.args(["fix", &f.prefix, "price_quote", "Net", &pol, "0"])
.output()
.expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(stdout.contains("NO CLEAN HOIST"), "must say no clean hoist exists, got:\n{stdout}");
assert!(stdout.contains("NEW ENTRY POINT"), "must offer the composition-root hoist, got:\n{stdout}");
assert!(stdout.contains("PROVABLY pure"), "must note the hoist is provably pure, got:\n{stdout}");
assert!(stdout.contains("fn/closure") && stdout.contains("trait"), "must recommend fn-injection over a trait port, got:\n{stdout}");
assert!(stdout.contains("Unknown"), "must name the fn-injection Unknown-hole trade-off, got:\n{stdout}");
assert!(stdout.contains("allow Net domain"), "must offer the policy-relax edit, got:\n{stdout}");
}
#[test]
fn fix_non_violation_is_a_no_op() {
let f = Fixture::new("fixok");
write_orderflow_fixture(&f);
let pol = write_policy(&f, "p.policy", "deny Net domain\n");
let out = Command::new(bin())
.args(["fix", &f.prefix, "get_quote", "Net", &pol, "0"])
.output()
.expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(stdout.contains("no policy forbids it"), "must report a non-violation, got:\n{stdout}");
}
#[test]
fn fix_unreadable_policy_exits_2() {
let f = Fixture::new("fixbadpol");
write_orderflow_fixture(&f);
let bogus = f.dir.join("typo.policy");
let out = Command::new(bin())
.args(["fix", &f.prefix, "price_quote", "Net", bogus.to_string_lossy().as_ref(), "0"])
.output()
.expect("run candor-query");
assert_eq!(out.status.code(), Some(2), "an unreadable policy must exit 2, not emit a plan");
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(stderr.contains("could not be read"), "must report the read failure, got:\n{stderr}");
}
#[test]
fn fix_no_policy_exits_2() {
let f = Fixture::new("fixnopol");
write_orderflow_fixture(&f);
let out = Command::new(bin())
.args(["fix", &f.prefix, "price_quote", "Net"])
.env_remove("CANDOR_POLICY")
.output()
.expect("run candor-query");
assert_eq!(out.status.code(), Some(2), "no policy must exit 2");
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(stderr.contains("policy is required"), "must explain a policy is required, got:\n{stderr}");
}
#[test]
fn fix_gate_collapses_inheritors_to_one_remedy() {
let f = Fixture::new("fixgate");
write_orderflow_fixture(&f);
let pol = write_policy(&f, "p.policy", "deny Net domain\n");
let out = Command::new(bin())
.args(["fix-gate", &f.prefix, &pol, "1"])
.output()
.expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value =
serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).expect("fix-gate --json must emit JSON");
assert_eq!(v["ok"], serde_json::json!(false), "a crossing exists → not ok");
let rem = v["remedies"].as_array().expect("remedies array");
assert_eq!(rem.len(), 1, "the two domain inheritors collapse to one remedy, got {}", rem.len());
assert_eq!(rem[0]["hoistTo"], serde_json::json!(["api::get_quote"]));
assert_eq!(rem[0]["site"], serde_json::json!(["infra::fetch_rate"]));
}
#[test]
fn fix_gate_clean_report_is_ok() {
let f = Fixture::new("fixgateok");
write_orderflow_fixture(&f);
let pol = write_policy(&f, "p.policy", "deny Net nonexistentlayer\n");
let out = Command::new(bin())
.args(["fix-gate", &f.prefix, &pol, "1"])
.output()
.expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert_eq!(v["ok"], serde_json::json!(true), "no crossing → ok");
assert_eq!(v["remedies"].as_array().unwrap().len(), 0, "no remedies when clean");
}
#[test]
fn fix_gate_strict_exits_1_on_a_crossing_advisory_otherwise() {
let f = Fixture::new("fixgatestrict");
write_orderflow_fixture(&f);
let pol = write_policy(&f, "p.policy", "deny Net domain\n");
let adv = Command::new(bin()).args(["fix-gate", "--report", &f.prefix, "--policy", &pol])
.output().expect("run candor-query");
assert_eq!(adv.status.code(), Some(0), "fix-gate is advisory by default (exit 0)");
let strict = Command::new(bin()).args(["fix-gate", "--report", &f.prefix, "--policy", &pol, "--strict"])
.output().expect("run candor-query");
assert_eq!(strict.status.code(), Some(1), "--strict with an outstanding crossing must exit 1");
let sj = Command::new(bin()).args(["fix-gate", "--report", &f.prefix, "--policy", &pol, "--strict", "--json"])
.output().expect("run candor-query");
assert_eq!(sj.status.code(), Some(1), "--strict --json with a crossing exits 1");
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(sj.stdout).unwrap()).unwrap();
assert_eq!(v["ok"], serde_json::json!(false), "ok is still false regardless of --strict");
let cleanpol = write_policy(&f, "clean.policy", "deny Net nonexistentlayer\n");
let clean = Command::new(bin()).args(["fix-gate", "--report", &f.prefix, "--policy", &cleanpol, "--strict"])
.output().expect("run candor-query");
assert_eq!(clean.status.code(), Some(0), "--strict over a clean report exits 0");
}
#[test]
fn unverified_and_fix_gate_answer_a_judged_nothing_report_at_exit_0_with_the_caveat() {
let f = Fixture::new("judgednothing");
let judged_nothing = r#"{"candor":{"version":"t","toolchain":"stable","spec":"0.28"},"package":"lib","functions":[],"analyzed":{"count":0,"digest":"0"}}"#;
let report_file = format!("{}.lib.scan.json", f.prefix);
std::fs::write(&report_file, judged_nothing).unwrap();
let pol = write_policy(&f, "p.policy", "deny Net app\n");
for verb in ["unverified", "fix-gate"] {
let out = Command::new(bin())
.args([verb, "--report", &f.prefix, "--policy", &pol, "--json", "--strict"])
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0),
"{verb} --strict must ANSWER a judged-nothing report at exit 0, not refuse at 2 — \
count-0 is a disclosure, not an exit code (⟨0.24⟩)");
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap())
.unwrap_or_else(|_| panic!("{verb} must emit a JSON document over a judged-nothing report"));
assert_eq!(v["incomplete"], serde_json::json!(true), "{verb}: the caveat flag");
assert_eq!(v["judgedNothing"], serde_json::json!([report_file]),
"{verb}: `judgedNothing` is an ARRAY OF REPORT PATHS (SPEC §2 ⟨0.28⟩), never a boolean");
assert!(v.get("ok").is_none(),
"{verb}: `ok` is withheld — a judged-nothing report licenses it no more than an unanalyzed one");
let prose = Command::new(bin())
.args([verb, "--report", &f.prefix, "--policy", &pol, "--strict"])
.output().expect("run candor-query");
assert_eq!(prose.status.code(), Some(0),
"{verb} prose --strict must exit 0 like its own --json channel over identical bytes");
let text = String::from_utf8_lossy(&prose.stdout).into_owned();
assert!(text.contains("JUDGED NOTHING"), "{verb}: the prose caveat must name the cause");
assert!(!text.contains("`gate --report` exits 2 over these bytes"),
"{verb}: the note claims the gate refuses, but `gate --report` exits 0 over a \
judged-nothing report — the disclosure discrediting itself");
let none = Command::new(bin())
.args([verb, "--report", &format!("{}-nowhere", f.prefix), "--policy", &pol, "--json"])
.output().expect("run candor-query");
assert_eq!(none.status.code(), Some(2),
"{verb}: a locator naming NO report is a loud failure, not a judged-nothing answer");
let healthy = Fixture::new(&format!("judgednothing-ctl-{verb}"));
healthy.write_report();
let hpol = write_policy(&healthy, "p.policy", "deny Net app\n");
let h = Command::new(bin())
.args([verb, "--report", &healthy.prefix, "--policy", &hpol, "--json", "--strict"])
.output().expect("run candor-query");
assert_eq!(h.status.code(), Some(0), "{verb}: healthy control exits 0");
let hv: serde_json::Value = serde_json::from_str(&String::from_utf8(h.stdout).unwrap()).unwrap();
assert_eq!(hv["ok"], serde_json::json!(true), "{verb}: healthy control keeps ok:true");
assert!(hv.get("incomplete").is_none() && hv.get("judgedNothing").is_none(),
"{verb}: a complete report must gain NO caveat key from this fix");
}
}
#[test]
fn a_report_with_no_analyzed_manifest_hedges_as_row_three_across_every_verb() {
let f = Fixture::new("nomanifest");
let pol = write_policy(&f, "p.policy", "deny Net app\n");
let mk = |suffix: &str, body: &str| {
let pre = format!("{}.{suffix}", f.prefix);
std::fs::write(format!("{pre}.lib.scan.json"), body).unwrap();
(pre.clone(), format!("{pre}.lib.scan.json"))
};
let (row3, row3_file) =
mk("row3", r#"{"candor":{"version":"t","spec":"0.20"},"package":"lib","functions":[]}"#);
let (row1, row1_file) = mk("row1",
r#"{"candor":{"version":"t","spec":"0.28"},"package":"lib","functions":[],"analyzed":{"count":0,"digest":"0"}}"#);
let (row2, _) = mk("row2",
r#"{"candor":{"version":"t","spec":"0.28"},"package":"lib","functions":[],"analyzed":{"count":7,"digest":"0"}}"#);
let json = |args: &[&str]| -> (i32, serde_json::Value) {
let out = Command::new(bin()).args(args).output().expect("run candor-query");
let text = String::from_utf8_lossy(&out.stdout).into_owned();
(out.status.code().unwrap(),
serde_json::from_str(&text).unwrap_or_else(|e| panic!("{args:?}: not JSON ({e}):\n{text}")))
};
for verb in [
vec!["where", "Fs"],
vec!["blindspots"],
vec!["reachable"],
vec!["map"],
vec!["unverified"],
vec!["fix-gate"],
] {
let mut a = verb.clone();
a.extend(["--report", &row3, "--policy", &pol, "--json"]);
let (rc, v) = json(&a);
assert_eq!(rc, 0, "{verb:?}: row 3 is a DISCLOSURE, not an exit code — the gate exits 0 too");
assert_eq!(v["incomplete"], serde_json::json!(true),
"{verb:?}: row 3's own instruction is `no manifest, no claim`: {v}");
assert_eq!(v["noManifest"], serde_json::json!([row3_file]),
"{verb:?}: SPEC §2 pins `noManifest: [\"<report path>\", …]` verbatim: {v}");
assert!(v.get("judgedNothing").is_none(),
"{verb:?}: the report DECLARES nothing — filing it under `judgedNothing` asserts an \
`analyzed.count: 0` that is not on the wire, and makes one key mean two things: {v}");
let mut a = verb.clone();
a.extend(["--report", &row1, "--policy", &pol, "--json"]);
let (rc, v) = json(&a);
assert_eq!(rc, 0);
assert_eq!(v["judgedNothing"], serde_json::json!([row1_file]),
"{verb:?}: the split goes both ways or it is a rename: {v}");
assert!(v.get("noManifest").is_none(), "{verb:?}: row 1 HAS a manifest; it declares 0: {v}");
let mut a = verb.clone();
a.extend(["--report", &row2, "--policy", &pol, "--json"]);
let (rc, v) = json(&a);
assert_eq!(rc, 0);
assert!(v.get("incomplete").is_none() && v.get("noManifest").is_none()
&& v.get("judgedNothing").is_none(),
"{verb:?}: row 2 MUST NOT hedge — over 1997 JVM dependency jars a predicate keyed on \
`functions` being empty would withdraw 104 real claims to catch 6: {v}");
}
let out = Command::new(bin()).args(["map", "--report", &row3]).output().expect("run");
let text = String::from_utf8_lossy(&out.stdout).into_owned();
assert!(text.contains("NO `analyzed` manifest"), "the prose must name the real cause:\n{text}");
assert!(!text.contains("JUDGED NOTHING") && !text.contains("analyzed.count: 0"),
"…and must not re-assert row 1's claim in prose after removing it from the wire:\n{text}");
let (_, v) = json(&["gains", &row3, &row1, "--json"]);
assert_eq!(v["noManifest"], serde_json::json!([row3_file]), "{v}");
assert_eq!(v["baselineJudgedNothing"], serde_json::json!([row1_file]), "{v}");
assert!(v.get("judgedNothing").is_none() && v.get("baselineNoManifest").is_none(), "{v}");
let (_, v) = json(&["gains", &row1, &row3, "--json"]);
assert_eq!(v["judgedNothing"], serde_json::json!([row1_file]), "{v}");
assert_eq!(v["baselineNoManifest"], serde_json::json!([row3_file]), "{v}");
let (row3full, _) = mk("row3full",
r#"{"candor":{"version":"t","spec":"0.20"},"package":"lib","functions":[{"fn":"app.reads","inferred":["Fs"],"direct":["Fs"]}]}"#);
let (rc, v) = json(&["where", "Fs", "--report", &row3full, "--json"]);
assert_eq!(rc, 0);
assert_eq!(v["directly"], serde_json::json!(["app.reads"]), "{v}");
assert!(v.get("incomplete").is_none() && v.get("noManifest").is_none(),
"a manifest-less report that LISTS entries is not hedging: {v}");
}
#[test]
fn gains_strict_exits_1_and_rejects_silently_swallowed_policy() {
let f = Fixture::new("gainsstrict");
let base_pre = format!("{}.base", f.prefix);
let cur_pre = format!("{}.cur", f.prefix);
let base_report = r#"{"candor":{"version":"t","spec":"0.23"},"package":"lib","functions":[{"fn":"lib::f","loc":"s:1","inferred":["Fs"],"hash":"h"}]}"#;
let cur_report = r#"{"candor":{"version":"t","spec":"0.23"},"package":"lib","functions":[{"fn":"lib::f","loc":"s:1","inferred":["Fs","Net"],"hash":"h"}]}"#;
std::fs::write(format!("{base_pre}.lib.scan.json"), base_report).unwrap();
std::fs::write(format!("{cur_pre}.lib.scan.json"), cur_report).unwrap();
let (curs, bases) = (cur_pre.clone(), base_pre.clone());
let adv = Command::new(bin()).args(["gains", &curs, &bases]).output().expect("run");
assert_eq!(adv.status.code(), Some(0), "gains is advisory by default (exit 0)");
assert!(String::from_utf8_lossy(&adv.stdout).contains("Net"), "gained Net must be listed");
let strict = Command::new(bin()).args(["gains", &curs, &bases, "--strict"]).output().expect("run");
assert_eq!(strict.status.code(), Some(1), "--strict with a gained effect must exit 1");
let pol = Command::new(bin()).args(["gains", &curs, &bases, "--policy", "/x"]).output().expect("run");
assert_eq!(pol.status.code(), Some(2), "an unknown flag (--policy) must be rejected, not swallowed");
let se = String::from_utf8_lossy(&pol.stderr);
assert!(se.contains("unknown flag") && se.contains("gained"),
"must name the unknown flag + point at the `deny <E> gained` scan gate, got:\n{se}");
let dash = Command::new(bin()).args(["gains", &curs, &bases, "-strict"]).output().expect("run");
assert_eq!(dash.status.code(), Some(2), "a single-dash typo (`-strict`) must reject, not silently drop");
let txt = Command::new(bin()).args(["gains", &curs, &bases, "--text"]).output().expect("run");
assert_ne!(txt.status.code(), Some(2), "--text (a valid cross-engine flag) must be tolerated by gains");
}
#[test]
fn valueless_policy_flag_exits_2_not_silent() {
let f = Fixture::new("valpol");
f.write_report();
let out = Command::new(bin()).args(["where", "Fs", "--report", &f.report_path(), "--policy"])
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(2), "a valueless --policy must exit 2");
assert!(String::from_utf8_lossy(&out.stderr).contains("--policy requires"),
"must name the missing argument");
}
#[test]
fn fix_gate_unreadable_policy_exits_2() {
let f = Fixture::new("fixgatebadpol");
write_orderflow_fixture(&f);
let bogus = f.dir.join("typo.policy");
let out = Command::new(bin())
.args(["fix-gate", &f.prefix, bogus.to_string_lossy().as_ref(), "1"])
.output()
.expect("run candor-query");
assert_eq!(out.status.code(), Some(2), "an unreadable policy must exit 2");
}
#[test]
fn unverified_flags_an_unknown_in_a_deny_scope() {
let f = Fixture::new("fixunv");
let report = r#"{
"candor": { "version": "scan-test", "toolchain": "stable", "spec": "0.23" },
"package": "of",
"functions": [
{ "fn": "domain::price", "loc": "src/d.rs:1:1", "inferred": ["Unknown"], "unknownWhy": ["callback:injected"], "hash": "of#p", "paths": ["/x"] },
{ "fn": "domain::calc", "loc": "src/d.rs:9:1", "inferred": [], "hash": "of#c", "paths": ["/x"] }
]
}"#;
std::fs::write(format!("{}.of.scan.json", f.prefix), report).unwrap();
let pol = write_policy(&f, "p.policy", "deny Net domain\n");
let out = Command::new(bin())
.args(["unverified", &f.prefix, &pol, "1"])
.output()
.expect("run candor-query");
assert_eq!(out.status.code(), Some(0), "advisory by default → exit 0");
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert_eq!(v["ok"], serde_json::json!(false), "an Unknown-in-scope hole exists");
let items = v["unverified"].as_array().unwrap();
assert_eq!(items.len(), 1, "only the Unknown fn is flagged, not the provably-pure one");
assert_eq!(items[0]["fn"], serde_json::json!("domain::price"));
assert_eq!(items[0]["upgrade"], serde_json::json!("deny Net Unknown domain"));
let strict = Command::new(bin())
.args(["unverified", &f.prefix, &pol, "--strict"])
.output()
.expect("run candor-query");
assert_eq!(strict.status.code(), Some(1), "--strict must exit 1 on an unverified hole");
}
#[test]
fn unverified_provably_pure_scope_is_clean() {
let f = Fixture::new("fixunvok");
let report = r#"{
"candor": { "version": "scan-test", "toolchain": "stable", "spec": "0.23" },
"package": "of",
"functions": [
{ "fn": "domain::calc", "loc": "src/d.rs:1:1", "inferred": [], "hash": "of#c", "paths": ["/x"] }
]
}"#;
std::fs::write(format!("{}.of.scan.json", f.prefix), report).unwrap();
let pol = write_policy(&f, "p.policy", "deny Net domain\n");
let out = Command::new(bin())
.args(["unverified", &f.prefix, &pol, "--strict", "1"])
.output()
.expect("run candor-query");
assert_eq!(out.status.code(), Some(0), "no Unknown holes → clean, exit 0 even under --strict");
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert_eq!(v["ok"], serde_json::json!(true));
}
const DOT_FREE_REASON: &str = "untyped cross-package receiver";
fn write_frontier_fixture(f: &Fixture, with_hierarchy: bool) {
let report = r#"{
"candor": { "version": "scan-test", "toolchain": "stable", "spec": "0.23" },
"package": "app",
"functions": [
{ "fn": "mod.Target.work", "inferred": ["Fs"], "direct": ["Fs"] },
{ "fn": "mod.Sub.handle", "inferred": ["Fs"], "calls": ["mod.Target.work"] },
{ "fn": "mod.Caller.run", "inferred": ["Unknown"], "unknownWhy": ["dispatch:mod.Base.handle"] },
{ "fn": "mod.Other.run", "inferred": ["Unknown"], "unknownWhy": ["dispatch:mod.Unrelated.frob"] },
{ "fn": "mod.NotSub.run", "inferred": ["Unknown"], "unknownWhy": ["dispatch:mod.Elsewhere.handle"] },
{ "fn": "mod.NoWhy.run", "inferred": ["Unknown"], "unknownWhy": ["callback:opaque fn pointer"] },
{ "fn": "mod.Dotfree.run", "inferred": ["Unknown"], "unknownWhy": ["dispatch:untyped cross-package receiver"] }
]
}"#;
std::fs::write(format!("{}.app.scan.json", f.prefix), report).unwrap();
std::fs::write(
format!("{}.app.scan.callgraph.json", f.prefix),
r#"{"mod.Sub.handle":["mod.Target.work"],"mod.Target.work":[]}"#,
)
.unwrap();
if with_hierarchy {
std::fs::write(
format!("{}.app.hierarchy.json", f.prefix),
r#"{"mod.Sub":["mod.Base"],"mod.Base":[]}"#,
)
.unwrap();
}
}
#[test]
fn callers_include_unknown_discloses_the_dispatch_frontier_via_the_hierarchy() {
let f = Fixture::new("frontier-hier");
write_frontier_fixture(&f, true);
let out = Command::new(bin())
.arg("callers").arg(&f.prefix).arg("work").arg("1").arg("--include-unknown")
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value =
serde_json::from_str(String::from_utf8(out.stdout).unwrap().trim()).expect("json");
assert_eq!(v["of"], serde_json::json!(["mod.Target.work"]));
assert_eq!(v["direct"], serde_json::json!(["mod.Sub.handle"]));
assert_eq!(v["transitive"], serde_json::json!(["mod.Sub.handle"]),
"frontier candidates must NOT be asserted into the confirmed set: {v}");
let poss = v["possibleViaUnknownDispatch"].as_array().expect("frontier array");
assert_eq!(poss.len(), 2, "the hierarchy-confirmed overrider + the unanswerable dot-free one: {v}");
assert_eq!(poss[0]["fn"], "mod.Caller.run");
assert_eq!(poss[0]["viaDispatchOn"], "handle");
assert_eq!(poss[1]["fn"], "mod.Dotfree.run");
assert_eq!(poss[1]["viaDispatchOn"], DOT_FREE_REASON);
}
#[test]
fn callers_include_unknown_without_hierarchy_over_lists_by_simple_name() {
let f = Fixture::new("frontier-flat");
write_frontier_fixture(&f, false);
let out = Command::new(bin())
.arg("callers").arg(&f.prefix).arg("work").arg("1").arg("--include-unknown")
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value =
serde_json::from_str(String::from_utf8(out.stdout).unwrap().trim()).expect("json");
let fns: Vec<&str> = v["possibleViaUnknownDispatch"].as_array().unwrap()
.iter().filter_map(|p| p["fn"].as_str()).collect();
assert_eq!(fns, vec!["mod.Caller.run", "mod.Dotfree.run", "mod.NotSub.run"],
"empty hierarchy must fall back to simple-name over-listing: {v}");
}
#[test]
fn callers_without_the_flag_omits_the_frontier_key() {
let f = Fixture::new("frontier-off");
write_frontier_fixture(&f, true);
let out = Command::new(bin())
.arg("callers").arg(&f.prefix).arg("work").arg("1")
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value =
serde_json::from_str(String::from_utf8(out.stdout).unwrap().trim()).expect("json");
assert!(v.get("possibleViaUnknownDispatch").is_none(),
"no --include-unknown → no frontier key: {v}");
assert_eq!(v["direct"], serde_json::json!(["mod.Sub.handle"]));
}
#[test]
fn callers_include_unknown_keys_off_the_kind_so_ambiguous_and_off_vocabulary_stay_out() {
let f = Fixture::new("frontier-kind-vs-class");
let report = r#"{
"candor": { "version": "scan-test", "toolchain": "stable", "spec": "0.23" },
"package": "app",
"functions": [
{ "fn": "app.Target.work", "inferred": ["Fs"], "direct": ["Fs"] },
{ "fn": "app.Sub.handle", "inferred": ["Fs"], "calls": ["app.Target.work"] },
{ "fn": "app.Real.run", "inferred": ["Unknown"], "direct": ["Unknown"], "unknownWhy": ["dispatch:app.Base.handle"] },
{ "fn": "app.Amb.run", "inferred": ["Unknown"], "direct": ["Unknown"], "unknownWhy": ["ambiguous:same-name local defs"] },
{ "fn": "app.Banana.run", "inferred": ["Unknown"], "direct": ["Unknown"], "unknownWhy": ["banana:whatever"] }
]
}"#;
std::fs::write(format!("{}.app.scan.json", f.prefix), report).unwrap();
std::fs::write(format!("{}.app.scan.callgraph.json", f.prefix),
r#"{"app.Sub.handle":["app.Target.work"],"app.Target.work":[]}"#).unwrap();
std::fs::write(format!("{}.app.hierarchy.json", f.prefix), r#"{"app.Sub":["app.Base"]}"#).unwrap();
assert_eq!(frontier_of(&f.prefix), vec![("app.Real.run".to_string(), "handle".to_string())],
"`ambiguous:` (class `dispatch`, no owner) and an off-vocabulary kind must both stay OUT");
let out = Command::new(bin())
.arg("blindspots").arg(&f.prefix).arg("--class").arg("dispatch").arg("--json")
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value = serde_json::from_str(String::from_utf8(out.stdout).unwrap().trim()).unwrap();
let names: Vec<&str> = v["sources"].as_array().unwrap().iter().map(|s| s["fn"].as_str().unwrap()).collect();
assert_eq!(names, ["app.Amb.run", "app.Real.run"],
"§6.2 projects `ambiguous:*` to class `dispatch` — a class-keyed frontier WOULD admit it: {v}");
let out = Command::new(bin())
.arg("blindspots").arg(&f.prefix).arg("--class").arg("unresolved").arg("--json")
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value = serde_json::from_str(String::from_utf8(out.stdout).unwrap().trim()).unwrap();
let names: Vec<&str> = v["sources"].as_array().unwrap().iter().map(|s| s["fn"].as_str().unwrap()).collect();
assert_eq!(names, ["app.Banana.run"], "an unrecognised kind classifies `unresolved`, never dropped: {v}");
assert_eq!(v["sources"][0]["why"], serde_json::json!(["banana:whatever"]),
"the fabricated kind must round-trip verbatim: {v}");
}
fn frontier_of(prefix: &str) -> Vec<(String, String)> {
frontier_of_q(prefix, "work")
}
fn frontier_of_q(prefix: &str, q: &str) -> Vec<(String, String)> {
let out = Command::new(bin())
.arg("callers").arg(prefix).arg(q).arg("1").arg("--include-unknown")
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value =
serde_json::from_str(String::from_utf8(out.stdout).unwrap().trim()).expect("json");
v["possibleViaUnknownDispatch"].as_array().expect("frontier array").iter()
.map(|p| (p["fn"].as_str().unwrap().to_string(),
p["viaDispatchOn"].as_str().unwrap().to_string()))
.collect()
}
#[test]
fn callers_include_unknown_discloses_a_dot_free_dispatch_reason_verbatim_in_both_arms() {
for with_hier in [true, false] {
let f = Fixture::new(if with_hier { "frontier-dotfree-hier" } else { "frontier-dotfree-flat" });
write_frontier_fixture(&f, with_hier);
let got = frontier_of(&f.prefix);
let entry = got.iter().find(|(fname, _)| fname == "mod.Dotfree.run");
assert!(entry.is_some(),
"dot-free dispatch source must be disclosed (hierarchy={with_hier}): {got:?}");
assert_eq!(entry.unwrap().1, DOT_FREE_REASON,
"viaDispatchOn must be the RAW detail verbatim (hierarchy={with_hier})");
}
}
fn write_two_level_frontier_fixture(f: &Fixture, hier: &str) {
let report = r#"{
"candor": { "version": "scan-test", "toolchain": "stable", "spec": "0.23" },
"package": "app",
"functions": [
{ "fn": "mod.Target.work", "inferred": ["Fs"], "direct": ["Fs"] },
{ "fn": "mod.Sub.handle", "inferred": ["Fs"], "calls": ["mod.Target.work"] },
{ "fn": "mod.Caller.run", "inferred": ["Unknown"], "unknownWhy": ["dispatch:mod.Base.handle"] }
]
}"#;
std::fs::write(format!("{}.app.scan.json", f.prefix), report).unwrap();
std::fs::write(
format!("{}.app.scan.callgraph.json", f.prefix),
r#"{"mod.Sub.handle":["mod.Target.work"],"mod.Target.work":[]}"#,
)
.unwrap();
if !hier.is_empty() {
std::fs::write(format!("{}.app.hierarchy.json", f.prefix), hier).unwrap();
}
}
#[test]
fn callers_include_unknown_unindexed_type_is_unanswerable_not_no() {
let names = |hier: &str, tag: &str| -> Vec<String> {
let f = Fixture::new(tag);
write_two_level_frontier_fixture(&f, hier);
frontier_of(&f.prefix).into_iter().map(|(n, _)| n).collect()
};
let disclosed = vec!["mod.Caller.run".to_string()];
assert_eq!(
names(r#"{"mod.Sub":["mod.Mid"],"mod.Mid":["mod.Base"],"mod.Base":[]}"#, "sidecar-complete"),
disclosed);
assert_eq!(names(r#"{"mod.Sub":["mod.Mid"]}"#, "sidecar-partial"), disclosed);
assert_eq!(names("", "sidecar-absent"), disclosed);
assert_eq!(
names(r#"{"mod.Sub":["mod.Mid"],"mod.Mid":[],"mod.Base":[]}"#, "sidecar-answered-no"),
Vec::<String>::new());
assert_eq!(
names(r#"{"mod.Sub":["mod.Base","mod.Unseen"],"mod.Base":[]}"#, "sidecar-positive-dominates"),
disclosed);
}
#[test]
fn callers_include_unknown_dot_free_disclosure_is_not_a_blanket() {
let f = Fixture::new("frontier-dotfree-control");
write_frontier_fixture(&f, true);
let got = frontier_of(&f.prefix);
let fns: Vec<&str> = got.iter().map(|(n, _)| n.as_str()).collect();
assert_eq!(fns, vec!["mod.Caller.run", "mod.Dotfree.run"],
"an ANSWERED-no condition stays out; only the unanswerable one is added: {got:?}");
for out in ["mod.NoWhy.run", "mod.Other.run", "mod.NotSub.run"] {
assert!(!fns.contains(&out), "{out} must NOT be disclosed: {got:?}");
}
}
#[test]
fn callers_include_unknown_dot_free_reason_never_matches_a_dot_free_rust_qual() {
for with_hier in [true, false] {
let f = Fixture::new(if with_hier { "frontier-rustqual-hier" } else { "frontier-rustqual-flat" });
let report = r#"{
"candor": { "version": "scan-test", "toolchain": "stable", "spec": "0.23" },
"package": "app",
"functions": [
{ "fn": "app::Target::work", "inferred": ["Fs"], "direct": ["Fs"] },
{ "fn": "app::Sub::handle", "inferred": ["Fs"], "calls": ["app::Target::work"] },
{ "fn": "app::EqQual::run", "inferred": ["Unknown"], "unknownWhy": ["dispatch:app::Sub::handle"] },
{ "fn": "app::BareLeaf::run", "inferred": ["Unknown"], "unknownWhy": ["dispatch:handle"] },
{ "fn": "app::Prose::run", "inferred": ["Unknown"], "unknownWhy": ["dispatch:untyped cross-package receiver"] }
]
}"#;
std::fs::write(format!("{}.app.scan.json", f.prefix), report).unwrap();
std::fs::write(
format!("{}.app.scan.callgraph.json", f.prefix),
r#"{"app::Sub::handle":["app::Target::work"],"app::Target::work":[]}"#,
).unwrap();
if with_hier {
std::fs::write(format!("{}.app.hierarchy.json", f.prefix), r#"{"app::Sub":["app::Base"]}"#).unwrap();
}
let got = frontier_of(&f.prefix);
assert_eq!(
got,
vec![
("app::BareLeaf::run".to_string(), "handle".to_string()),
("app::EqQual::run".to_string(), "app::Sub::handle".to_string()),
("app::Prose::run".to_string(), DOT_FREE_REASON.to_string()),
],
"every dot-free detail is disclosed verbatim, arm-independently (hierarchy={with_hier}): {got:?}"
);
}
}
#[test]
fn callers_include_unknown_mixed_source_joins_members_and_raw_details_in_sorted_order() {
let f = Fixture::new("frontier-mixed");
let report = r#"{
"candor": { "version": "scan-test", "toolchain": "stable", "spec": "0.27" },
"package": "app",
"functions": [
{ "fn": "app.Sink.touch", "inferred": ["Fs"], "direct": ["Fs"] },
{ "fn": "app.Impl.run", "inferred": ["Fs"], "calls": ["app.Sink.touch"] },
{ "fn": "app.Zed.write", "inferred": ["Fs"], "calls": ["app.Sink.touch"] },
{ "fn": "app.Mixed.go", "inferred": ["Unknown"], "unknownWhy": [
"dispatch:untyped cross-package receiver",
"dispatch:app.Base.write",
"dispatch:app.Base.run" ] },
{ "fn": "app.Dedup.go", "inferred": ["Unknown"], "unknownWhy": [
"dispatch:app.Base.run",
"dispatch:app.Other.run" ] }
]
}"#;
std::fs::write(format!("{}.app.scan.json", f.prefix), report).unwrap();
std::fs::write(
format!("{}.app.scan.callgraph.json", f.prefix),
r#"{"app.Impl.run":["app.Sink.touch"],"app.Zed.write":["app.Sink.touch"],"app.Sink.touch":[]}"#,
).unwrap();
std::fs::write(
format!("{}.app.hierarchy.json", f.prefix),
r#"{"app.Impl":["app.Base","app.Other"],"app.Zed":["app.Base"]}"#,
).unwrap();
let got = frontier_of_q(&f.prefix, "touch");
assert_eq!(
got,
vec![
("app.Dedup.go".to_string(), "run".to_string()),
("app.Mixed.go".to_string(), "run,untyped cross-package receiver,write".to_string()),
],
"mixed source: sorted+deduplicated union of passing members and raw details: {got:?}"
);
}
#[test]
fn callers_include_unknown_join_sorts_by_unicode_code_point_not_utf16_code_unit() {
let f = Fixture::new("frontier-collation");
let report = r#"{
"candor": { "version": "scan-test", "toolchain": "stable", "spec": "0.27" },
"package": "app",
"functions": [
{ "fn": "app.Sink.touch", "inferred": ["Fs"], "direct": ["Fs"] },
{ "fn": "app.Impl.run", "inferred": ["Fs"], "calls": ["app.Sink.touch"] },
{ "fn": "app.Wide.go", "inferred": ["Unknown"], "unknownWhy": [
"dispatch:\ud83d\ude00-supplementary",
"dispatch:\uff21-bmp-tail" ] }
]
}"#;
std::fs::write(format!("{}.app.scan.json", f.prefix), report).unwrap();
std::fs::write(
format!("{}.app.scan.callgraph.json", f.prefix),
r#"{"app.Impl.run":["app.Sink.touch"],"app.Sink.touch":[]}"#,
).unwrap();
let got = frontier_of_q(&f.prefix, "touch");
assert_eq!(
got,
vec![("app.Wide.go".to_string(), "\u{FF21}-bmp-tail,\u{1F600}-supplementary".to_string())],
"code-point order puts U+FF21 before U+1F600; UTF-16 code-unit order would invert it: {got:?}"
);
}
#[test]
fn blindspots_ranks_sources_by_unknown_blast_radius() {
let f = Fixture::new("blindspots");
let report = r#"{
"candor": { "version": "scan-test", "toolchain": "stable", "spec": "0.23" },
"package": "bs",
"functions": [
{ "fn": "src_a", "inferred": ["Unknown"], "unknownWhy": ["callback:unresolved call"] },
{ "fn": "mid", "inferred": ["Unknown"], "calls": ["src_a"] },
{ "fn": "top", "inferred": ["Unknown"], "calls": ["mid"] },
{ "fn": "src_b", "inferred": ["Unknown"], "unknownWhy": ["native:extern fn"] }
]
}"#;
std::fs::write(f.report_path().replace(".rpt.", ".bs."), report).unwrap();
let out = Command::new(bin())
.arg("blindspots").arg(&f.prefix).arg("--json")
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value =
serde_json::from_str(String::from_utf8(out.stdout).unwrap().trim()).expect("json");
assert_eq!(v["totalUnknown"], 4, "every Unknown-carrying fn counts: {v}");
let sources = v["sources"].as_array().expect("sources");
assert_eq!(sources.len(), 2, "only unknownWhy CARRIERS are sources (mid/top are not): {v}");
assert_eq!(sources[0]["fn"], "src_a", "most-smearing source ranks first: {v}");
assert_eq!(sources[0]["reaches"], 2);
assert_eq!(sources[0]["affected"], serde_json::json!(["mid", "top"]));
assert_eq!(sources[0]["why"], serde_json::json!(["callback:unresolved call"]));
assert_eq!(sources[1]["fn"], "src_b");
assert_eq!(sources[1]["reaches"], 0);
let out = Command::new(bin())
.arg("blindspots").arg(&f.prefix)
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let text = String::from_utf8(out.stdout).unwrap();
assert!(text.contains("2 Unknown source(s) explaining 4 Unknown function(s)"),
"headline must count sources + explained fns, got:\n{text}");
}
#[test]
fn blindspots_stats_reason_class_distribution() {
let f = Fixture::new("blindspots-stats");
let report = r#"{
"candor": { "version": "scan-test", "toolchain": "stable", "spec": "0.23" },
"package": "bs",
"functions": [
{ "fn": "src_a", "inferred": ["Unknown"], "unknownWhy": ["callback:unresolved call"] },
{ "fn": "mid", "inferred": ["Unknown"], "calls": ["src_a"] },
{ "fn": "src_b", "inferred": ["Unknown"], "unknownWhy": ["native:extern fn"] }
]
}"#;
std::fs::write(f.report_path().replace(".rpt.", ".bs."), report).unwrap();
let out = Command::new(bin())
.arg("blindspots").arg(&f.prefix).arg("--stats").arg("--json")
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value =
serde_json::from_str(String::from_utf8(out.stdout).unwrap().trim()).expect("json");
for c in ["reflect", "dispatch", "indirect", "native", "unresolved", "setup"] {
assert!(v["byClass"].get(c).is_some(), "byClass must carry every class: {v}");
}
assert_eq!(v["byClass"]["indirect"], 1, "callback → indirect: {v}");
assert_eq!(v["byClass"]["native"], 1, "native → native: {v}");
assert_eq!(v["sources"], 2);
assert_eq!(v["totalUnknown"], 3);
let out = Command::new(bin())
.arg("blindspots").arg(&f.prefix).arg("--class").arg("native").arg("--json")
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value = serde_json::from_str(String::from_utf8(out.stdout).unwrap().trim()).unwrap();
let names: Vec<&str> = v["sources"].as_array().unwrap().iter().map(|s| s["fn"].as_str().unwrap()).collect();
assert_eq!(names, ["src_b"], "--class native keeps only the native source: {v}");
let out = Command::new(bin())
.arg("blindspots").arg(&f.prefix).arg("--stats").arg("--class").arg("indirect").arg("--json")
.output().expect("run candor-query");
let v: serde_json::Value = serde_json::from_str(String::from_utf8(out.stdout).unwrap().trim()).unwrap();
assert_eq!(v["sources"], 1, "--stats --class indirect counts only the indirect source: {v}");
assert_eq!(v["byClass"]["native"], 0);
}
#[test]
fn blindspots_clean_report_says_so_exit_0() {
let f = Fixture::new("blindspots-clean");
f.write_report(); let out = Command::new(bin())
.arg("blindspots").arg(&f.prefix)
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
assert!(String::from_utf8(out.stdout).unwrap().contains("no Unknown sources"));
}
#[test]
fn rewire_reports_a_dropped_edge_exit_1_and_clean_exit_0() {
let f = Fixture::new("rewire");
let base = f.dir.join("base").to_string_lossy().into_owned();
let cur = f.dir.join("cur").to_string_lossy().into_owned();
std::fs::write(format!("{base}.app.scan.callgraph.json"),
r#"{"api.handle":["pricing.quote","util.log"],"pricing.quote":[]}"#).unwrap();
std::fs::write(format!("{cur}.app.scan.callgraph.json"),
r#"{"api.handle":["util.log"],"pricing.quote":[]}"#).unwrap();
let out = Command::new(bin())
.arg("rewire").arg(&cur).arg(&base).arg("1")
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(1), "a dropped edge must exit 1 (verify the fix didn't gut the feature)");
let v: serde_json::Value =
serde_json::from_str(String::from_utf8(out.stdout).unwrap().trim()).expect("json");
assert_eq!(v["ok"], false);
assert_eq!(v["dropped"], serde_json::json!([
{"caller": "api.handle", "no_longer_calls": ["pricing.quote"]}
]), "exactly the dropped edge, not the kept one: {v}");
let out = Command::new(bin())
.arg("rewire").arg(&base).arg(&base).arg("0")
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
assert!(String::from_utf8(out.stdout).unwrap().contains("nothing de-wired"));
}
#[test]
fn rewire_missing_either_side_fails_loud_exit_2() {
let f = Fixture::new("rewire-miss");
let real = f.dir.join("real").to_string_lossy().into_owned();
std::fs::write(format!("{real}.app.scan.callgraph.json"), r#"{"a":["b"]}"#).unwrap();
let missing = f.dir.join("nosuch").to_string_lossy().into_owned();
for (cur, base) in [(&missing, &real), (&real, &missing)] {
let out = Command::new(bin())
.arg("rewire").arg(cur).arg(base).arg("0")
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(2), "a missing side must exit 2, not a false verdict");
}
}
#[test]
fn locate_finds_the_scan_binary_and_misses_cleanly() {
let f = Fixture::new("locate");
std::fs::write(f.dir.join("candor-scan"), b"#!fake").unwrap();
let out = Command::new(bin())
.arg("locate").arg("scan").arg(f.dir.to_string_lossy().as_ref())
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
assert!(String::from_utf8(out.stdout).unwrap().trim().ends_with("candor-scan"));
let out = Command::new(bin())
.arg("locate").arg("lib").arg(f.dir.to_string_lossy().as_ref())
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(1));
assert!(out.stdout.is_empty());
}
fn write_class_grammar_report(f: &Fixture) -> String {
let report = r#"{
"candor": { "version": "scan-test", "toolchain": "stable", "spec": "0.23" },
"package": "of",
"functions": [
{ "fn": "domain::src_dispatch", "loc": "src/d.rs:1:1", "inferred": ["Unknown"], "direct": ["Unknown"], "unknownWhy": ["dispatch:Base::run"], "hash": "of#sd", "paths": ["/x"] },
{ "fn": "domain::inherits", "loc": "src/d.rs:9:1", "inferred": ["Unknown"], "hash": "of#in", "paths": ["/x"], "calls": ["domain::src_dispatch"] },
{ "fn": "domain::src_native", "loc": "src/d.rs:17:1", "inferred": ["Unknown"], "direct": ["Unknown"], "unknownWhy": ["native:strlen"], "hash": "of#sn", "paths": ["/x"] }
]
}"#;
std::fs::write(format!("{}.of.scan.json", f.prefix), report).unwrap();
write_policy(f, "p.policy", "deny Exec domain\n")
}
fn unverified_names(prefix: &str, pol: &str, class: Option<&str>) -> (i32, Vec<String>) {
let mut args: Vec<String> =
vec!["unverified".into(), "--report".into(), prefix.into(), "--policy".into(), pol.into(), "--json".into()];
if let Some(c) = class {
args.push("--class".into());
args.push(c.into());
}
let out = Command::new(bin()).args(&args).output().expect("run candor-query");
let code = out.status.code().unwrap();
let text = String::from_utf8(out.stdout).unwrap();
let names = serde_json::from_str::<serde_json::Value>(text.trim())
.map(|v| {
v["unverified"].as_array().cloned().unwrap_or_default().iter()
.map(|h| h["fn"].as_str().unwrap_or_default().to_string())
.collect::<Vec<_>>()
})
.unwrap_or_default();
(code, names)
}
#[test]
fn class_grammar_accepts_a_token_a_list_and_both_aliases() {
let f = Fixture::new("class-grammar-ok");
let pol = write_class_grammar_report(&f);
let (code, all) = unverified_names(&f.prefix, &pol, None);
assert_eq!(code, 0);
let mut all_sorted = all.clone();
all_sorted.sort();
assert_eq!(all_sorted, ["domain::inherits", "domain::src_dispatch", "domain::src_native"],
"the unfiltered baseline the filters are a subset of");
let (code, mut got) = unverified_names(&f.prefix, &pol, Some("dispatch"));
got.sort();
assert_eq!((code, got.as_slice()), (0, ["domain::inherits".to_string(), "domain::src_dispatch".to_string()].as_slice()),
"--class dispatch selects the dispatch source + its inheritor, and nothing else");
let (code, list) = unverified_names(&f.prefix, &pol, Some("dispatch,native"));
assert_eq!(code, 0);
assert_eq!(list.len(), 3, "--class dispatch,native unions the two classes: {list:?}");
let (code, spaced) = unverified_names(&f.prefix, &pol, Some(" dispatch , native "));
assert_eq!((code, spaced.len()), (0, 3), "a spaced list is the same list");
let (code, dynamic) = unverified_names(&f.prefix, &pol, Some("dynamic"));
assert_eq!((code, dynamic.len()), (0, 3), "`dynamic` is every genuine class; no fixture entry is setup-only");
let (code, star) = unverified_names(&f.prefix, &pol, Some("*"));
assert_eq!((code, star.len()), (0, 3), "`*` is all six classes");
let (code, none) = unverified_names(&f.prefix, &pol, Some("reflect"));
assert_eq!((code, none.len()), (0, 0), "a valid class with no candidate is an empty answer, not an error");
}
#[test]
fn class_grammar_refuses_an_unrecognised_token_exit_2() {
let f = Fixture::new("class-grammar-typo");
let pol = write_class_grammar_report(&f);
for verb in [
vec!["unverified", "--report", &f.prefix, "--policy", &pol, "--json"],
vec!["blindspots", "--report", &f.prefix, "--json"],
] {
let mut args = verb.clone();
args.push("--class");
args.push("dyanmic"); let out = Command::new(bin()).args(&args).output().expect("run candor-query");
let err = String::from_utf8(out.stderr).unwrap();
assert_eq!(out.status.code(), Some(2), "{args:?} must be a usage error, got:\n{err}");
assert!(err.contains("dyanmic"), "the message must name the offending token, got:\n{err}");
assert!(err.contains("unrecognised reason-class"),
"…and say what is wrong with it, rather than exiting 2 for some other reason:\n{err}");
for t in ["reflect", "dispatch", "indirect", "native", "unresolved", "setup", "dynamic", "*"] {
assert!(err.contains(t), "the accepted set must list `{t}`, got:\n{err}");
}
assert!(out.stdout.is_empty(), "a refused --class must emit no answer at all: {:?}", String::from_utf8(out.stdout));
}
}
#[test]
fn class_grammar_refuses_a_repeated_flag_exit_2() {
let f = Fixture::new("class-grammar-repeat");
let pol = write_class_grammar_report(&f);
for verb in [
vec!["unverified", "--report", &f.prefix, "--policy", &pol, "--json"],
vec!["blindspots", "--report", &f.prefix, "--json"],
] {
let mut args = verb.clone();
args.extend(["--class", "unresolved", "--class", "native"]);
let out = Command::new(bin()).args(&args).output().expect("run candor-query");
let err = String::from_utf8(out.stderr).unwrap();
assert_eq!(out.status.code(), Some(2), "a repeated --class must be a usage error, got:\n{err}");
assert!(err.contains("more than once"), "the message must say the flag was repeated, got:\n{err}");
assert!(err.contains("not a union"),
"…and that the two lists are not unioned — last-wins and union are BOTH silent misreadings:\n{err}");
assert!(!err.contains("unrecognised reason-class"),
"`unresolved` and `native` are both valid tokens; this is the repeat rule, not the token rule:\n{err}");
assert!(out.stdout.is_empty(), "a refused --class must emit no answer at all");
}
}
#[test]
fn blindspots_class_grammar_keeps_its_selection() {
let f = Fixture::new("class-grammar-bs");
write_class_grammar_report(&f);
let names = |class: Option<&str>| -> (i32, Vec<String>) {
let mut args: Vec<String> = vec!["blindspots".into(), "--report".into(), f.prefix.clone(), "--json".into()];
if let Some(c) = class {
args.push("--class".into());
args.push(c.into());
}
let out = Command::new(bin()).args(&args).output().expect("run candor-query");
let code = out.status.code().unwrap();
let text = String::from_utf8(out.stdout).unwrap();
let v: serde_json::Value = serde_json::from_str(text.trim()).unwrap_or(serde_json::json!({}));
let mut n: Vec<String> = v["sources"].as_array().cloned().unwrap_or_default().iter()
.map(|s| s["fn"].as_str().unwrap_or_default().to_string()).collect();
n.sort();
(code, n)
};
assert_eq!(names(None), (0, vec!["domain::src_dispatch".to_string(), "domain::src_native".to_string()]),
"both DIRECT sources; `domain::inherits` is inherited-only and is not a source");
assert_eq!(names(Some("native")), (0, vec!["domain::src_native".to_string()]));
assert_eq!(names(Some("dispatch,native")), (0, vec!["domain::src_dispatch".to_string(), "domain::src_native".to_string()]));
assert_eq!(names(Some("dynamic")).1.len(), 2, "`dynamic` drops nothing here — no setup-class source");
assert_eq!(names(Some("*")).1.len(), 2);
assert_eq!(names(Some("reflect")), (0, vec![]), "a valid class with no source is an empty answer");
}
fn gate_fixture(dir: &std::path::Path, sub: &str, report: &str, callgraph: Option<&str>) -> String {
let d = dir.join(sub);
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
std::fs::write(d.join("report.app.scan.json"), report).unwrap();
if let Some(cg) = callgraph {
std::fs::write(d.join("report.app.scan.callgraph.json"), cg).unwrap();
}
d.join("report").to_string_lossy().into_owned()
}
fn run_gate(locator: &str, policy: &std::path::Path, extra: &[&str]) -> (i32, String, String) {
let mut args: Vec<String> = vec![
"gate".into(),
"--report".into(),
locator.into(),
"--policy".into(),
policy.to_string_lossy().into_owned(),
];
args.extend(extra.iter().map(|s| s.to_string()));
let out = Command::new(bin()).args(&args).output().expect("run candor-query");
(
out.status.code().unwrap_or(-1),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
fn pol(dir: &std::path::Path, name: &str, text: &str) -> std::path::PathBuf {
let p = dir.join(format!("{name}.policy"));
std::fs::write(&p, text).unwrap();
p
}
#[test]
fn gate_report_does_not_backfill_an_absent_entry_and_the_control_fires() {
let f = Fixture::new("gate-mustnot");
let absent = r#"{"candor":{"version":"handwritten","spec":"0.23"},"package":"app",
"analyzed":{"count":3,"digest":"0"},
"functions":[{"fn":"app.visible","inferred":["Net"],"direct":["Net"],
"hosts":["example.com"],"netClass":["unknown-host"]}]}"#;
let present = r#"{"candor":{"version":"handwritten","spec":"0.23"},"package":"app",
"analyzed":{"count":3,"digest":"0"},
"functions":[{"fn":"app.visible","inferred":["Net"],"direct":["Net"],
"hosts":["example.com"],"netClass":["unknown-host"]},
{"fn":"app.hidden","inferred":["Fs"],"direct":["Fs"],"paths":["/etc/hosts"]}]}"#;
let cg = r#"{"app.visible":[],"app.hidden":["dep.readCfg"],"dep.readCfg":[]}"#;
let dep = f.dir.join("dep.json");
std::fs::write(
&dep,
r#"{"candor":{"version":"handwritten","spec":"0.23"},"package":"dep",
"analyzed":{"count":1,"digest":"0"},
"functions":[{"fn":"dep.readCfg","inferred":["Fs"],"direct":["Fs"],"paths":["/etc/hosts"]}]}"#,
)
.unwrap();
let deny_fs = pol(&f.dir, "denyfs", "deny Fs\n");
let mut codes = Vec::new();
for (sub, body) in [("absent", absent), ("present", present)] {
let loc = gate_fixture(&f.dir, sub, body, Some(cg));
let cfg = f.dir.join(sub).join(".candor");
std::fs::create_dir_all(&cfg).unwrap();
std::fs::write(cfg.join("config"), format!("deps = {}\n", dep.display())).unwrap();
let out = Command::new(bin())
.args(["gate", "--report", &loc, "--policy", &deny_fs.to_string_lossy()])
.env("CANDOR_DEPS", &dep) .output()
.expect("run candor-query");
codes.push((sub, out.status.code().unwrap_or(-1), String::from_utf8_lossy(&out.stderr).into_owned()));
}
assert_eq!(
codes[0].1, 0,
"an ABSENT entry was back-filled: `deny Fs` fired over a report carrying no Fs, with the \
callgraph sidecar + CANDOR_DEPS + config `deps` all supplying it\n{}",
codes[0].2
);
assert_eq!(
codes[1].1, 1,
"NEGATIVE CONTROL: the same policy over a report that DOES carry Fs must exit 1 — without it \
this row cannot tell 'not back-filled' from 'policy never evaluated'\n{}",
codes[1].2
);
}
#[test]
fn gate_report_refuses_forbid_and_allow_whole_policy() {
let f = Fixture::new("gate-refuse-policy");
let loc = gate_fixture(
&f.dir,
"r",
r#"{"candor":{"version":"handwritten","spec":"0.23"},"package":"app",
"analyzed":{"count":1,"digest":"0"},
"functions":[{"fn":"app.egress","inferred":["Net"],"direct":["Net"],
"hosts":["example.com"],"netClass":["unknown-host"]}]}"#,
None,
);
for (name, text, needle) in [
("forbid", "forbid app -> dep\n", "`forbid`"),
("allow", "allow Net example.com\n", "allow "),
("mixed_no_hit", "deny Exec\nforbid app -> dep\n", "`forbid`"),
] {
let (rc, stdout, err) = run_gate(&loc, &pol(&f.dir, name, text), &[]);
assert_eq!(rc, 2, "{name} must be REFUSED (exit 2), never evaluated:\n{err}");
assert!(err.contains(needle), "the refusal must name the offending rule kind, got:\n{err}");
assert!(err.contains("scan time"), "…and carry the remedy (gate at scan time), got:\n{err}");
assert!(stdout.is_empty(), "a refused policy must produce no verdict at all");
}
let (rc, _, err) = run_gate(&loc, &pol(&f.dir, "bare", "deny Net\n"), &[]);
assert_eq!(rc, 1, "the answerable control must FIRE, or the refusals prove nothing:\n{err}");
for (name, text) in
[("dom_forbid", "deny Net\nforbid app -> dep\n"), ("dom_allow", "deny Net\nallow Net other.example.com\n")]
{
let out = f.dir.join(format!("{name}.json"));
let _ = std::fs::remove_file(&out);
let (rc, _, err) =
run_gate(&loc, &pol(&f.dir, name, text), &["--gate-json", &out.to_string_lossy()]);
assert_eq!(rc, 1, "{name}: a firing `deny` dominates a whole-policy refusal:\n{err}");
let doc = std::fs::read_to_string(&out)
.unwrap_or_else(|e| panic!("{name}: the certain violation must reach the document ({e}):\n{err}"));
let v: serde_json::Value = serde_json::from_str(&doc).unwrap();
let fns: Vec<&str> =
v["violations"].as_array().unwrap().iter().map(|x| x["fn"].as_str().unwrap()).collect();
assert_eq!(fns, vec!["app.egress"], "{name}: the exit code is one bit, the document is the evidence:\n{doc}");
assert!(err.contains("scan time"), "{name}: the dominated refusal must still be named:\n{err}");
}
}
#[test]
fn gate_report_refuses_a_scoped_deny_whose_scoping_datum_is_absent() {
let f = Fixture::new("gate-refuse-scoped");
let net = gate_fixture(
&f.dir,
"net",
r#"{"candor":{"version":"handwritten","spec":"0.23"},"package":"app",
"analyzed":{"count":1,"digest":"0"},
"functions":[{"fn":"app.egress","inferred":["Net"],"direct":["Net"],"hosts":["example.com"]}]}"#,
None,
);
let unk = gate_fixture(
&f.dir,
"unk",
r#"{"candor":{"version":"handwritten","spec":"0.23"},"package":"app",
"analyzed":{"count":1,"digest":"0"},
"functions":[{"fn":"app.murky","inferred":["Unknown"]}]}"#,
None,
);
let cases = [
(&net, "netscoped", "deny Net[unknown-host]\n", 2, "netbare", "deny Net\n"),
(&unk, "unkscoped", "deny Unknown[dispatch]\n", 2, "unkbare", "deny Unknown\n"),
];
for (loc, sname, stext, swant, bname, btext) in cases {
let (rc, _, err) = run_gate(loc, &pol(&f.dir, sname, stext), &[]);
assert_eq!(rc, swant, "the scoped rule must be REFUSED, not silently narrowed:\n{err}");
assert!(err.contains("Refusing"), "the refusal must say so, got:\n{err}");
let (brc, _, berr) = run_gate(loc, &pol(&f.dir, bname, btext), &[]);
assert_eq!(brc, 1, "the BARE rule must fire — else the scoped exit 2 proves nothing:\n{berr}");
}
let carried = gate_fixture(
&f.dir,
"carried",
r#"{"candor":{"version":"handwritten","spec":"0.23"},"package":"app",
"analyzed":{"count":1,"digest":"0"},
"functions":[{"fn":"app.egress","inferred":["Net"],"direct":["Net"],
"hosts":["example.com"],"netClass":["unknown-host"]}]}"#,
None,
);
let (rc, _, err) = run_gate(&carried, &pol(&f.dir, "netscoped2", "deny Net[unknown-host]\n"), &[]);
assert_eq!(rc, 1, "a scoped rule whose scoping datum is PRESENT must evaluate, not refuse:\n{err}");
let (rc, _, err) = run_gate(&carried, &pol(&f.dir, "nettel", "deny Net[known-telemetry]\n"), &[]);
assert_eq!(rc, 0, "…and tolerate when the class does not match:\n{err}");
}
#[test]
fn gate_report_reports_a_certain_violation_over_an_unanswerable_rule_beside_it() {
let f = Fixture::new("gate-precedence");
let loc = gate_fixture(
&f.dir,
"r",
r#"{"candor":{"version":"handwritten","spec":"0.23"},"package":"app",
"analyzed":{"count":2,"digest":"0"},
"functions":[
{"fn":"app.fsUnit","inferred":["Fs"],"direct":["Fs"],"paths":["/etc/x"]},
{"fn":"app.netNoClass","inferred":["Net"],"direct":["Net"],"hosts":["h.example.com"]}]}"#,
None,
);
let out = f.dir.join("verdict.json");
let _ = std::fs::remove_file(&out);
let both = pol(&f.dir, "both", "deny Fs app.fsUnit\ndeny Net[unknown-host] app.netNoClass\n");
let (rc, _, err) = run_gate(&loc, &both, &["--gate-json", &out.to_string_lossy()]);
assert_eq!(rc, 1, "a rule that FIRES on evidence the report carries dominates the refusal:\n{err}");
let doc = std::fs::read_to_string(&out)
.unwrap_or_else(|e| panic!("a refusal must not delete the verdict document ({e}):\n{err}"));
let v: serde_json::Value = serde_json::from_str(&doc).unwrap();
assert_eq!(v["ok"], false);
let fns: Vec<&str> =
v["violations"].as_array().unwrap().iter().map(|x| x["fn"].as_str().unwrap()).collect();
assert_eq!(
fns,
vec!["app.fsUnit"],
"THE CERTAIN VIOLATION MUST BE IN THE DOCUMENT — an exit code is one bit, the document is the \
evidence, and this is the channel a PR comment is built from:\n{doc}"
);
assert!(
err.contains("deny Net[unknown-host] app.netNoClass"),
"the dominated refusal must still name the rule it could not evaluate:\n{err}"
);
let sole = pol(&f.dir, "sole", "deny Net[unknown-host] app.netNoClass\n");
let (rc2, _, err2) = run_gate(&loc, &sole, &[]);
assert_eq!(rc2, 2, "the unanswerable rule alone must still REFUSE:\n{err2}");
let only_fs = pol(&f.dir, "onlyfs", "deny Fs app.fsUnit\n");
let (rc3, _, err3) = run_gate(&loc, &only_fs, &[]);
assert_eq!(rc3, 1, "the firing rule alone must exit 1:\n{err3}");
}
#[test]
fn every_policy_reasoning_verb_refuses_what_the_gate_refuses() {
let f = Fixture::new("verb-policy");
let loc = gate_fixture(
&f.dir,
"r",
r#"{"candor":{"version":"handwritten","spec":"0.24"},"package":"app",
"analyzed":{"count":1,"digest":"0"},
"functions":[{"fn":"app.nat","inferred":["Unknown"],"direct":["Unknown"],
"unknownWhy":["native:extern fn"]}]}"#,
Some(r#"{"app.nat":[]}"#),
);
std::fs::create_dir_all(f.dir.join(".candor")).unwrap();
let p = pol(&f.dir, "aliased", "deny Unknown[corp] app.nat\n");
let ps = p.to_string_lossy().into_owned();
let run = |verb: &str, extra: &[&str]| -> i32 {
let mut args: Vec<String> = vec![verb.into()];
args.extend(extra.iter().map(|s| s.to_string()));
args.extend(["--report".into(), loc.clone(), "--policy".into(), ps.clone()]);
Command::new(bin()).args(&args).output().expect("run candor-query").status.code().unwrap_or(-1)
};
let verbs: [(&str, &[&str]); 4] =
[("gate", &[]), ("whatif", &["app.nat", "Unknown"]), ("fix-gate", &[]), ("unverified", &[])];
std::fs::write(f.dir.join(".candor/config"), "unknown-alias corp = reflect\n").unwrap();
for (verb, extra) in verbs {
assert_ne!(run(verb, extra), 2, "`{verb}` must EVALUATE a policy whose alias resolves");
}
std::fs::write(f.dir.join(".candor/config"), "unknown-alias corp = dispatch,nativ\n").unwrap();
for (verb, extra) in verbs {
assert_eq!(
run(verb, extra),
2,
"`{verb}` must refuse a policy the GATE refuses — answering from a rule the gate will not \
apply is the worse failure in a verb consulted BEFORE the edit"
);
}
std::fs::write(f.dir.join(".candor/config"), "").unwrap();
let bad = pol(&f.dir, "badtok", "deny Unknown[dispatch,nativ] app.nat\n");
let bs = bad.to_string_lossy().into_owned();
for (verb, extra) in verbs {
let mut args: Vec<String> = vec![verb.into()];
args.extend(extra.iter().map(|s| s.to_string()));
args.extend(["--report".into(), loc.clone(), "--policy".into(), bs.clone()]);
let out = Command::new(bin()).args(&args).output().expect("run candor-query");
assert_eq!(out.status.code(), Some(2), "`{verb}`: an unrecognised token is a policy error");
assert!(
String::from_utf8_lossy(&out.stderr).contains("nativ"),
"`{verb}`: …and the refusal must name the token"
);
}
}
#[test]
fn gate_report_claims_a_firing_rule_only_where_one_actually_fired() {
let f = Fixture::new("gate-claim");
let loc = gate_fixture(
&f.dir,
"r",
r#"{"candor":{"version":"handwritten","spec":"0.24"},"package":"app",
"analyzed":{"count":2,"digest":"0"},
"functions":[
{"fn":"app.murky","inferred":["Unknown"]},
{"fn":"app.writes","inferred":["Fs"],"direct":["Fs"],"paths":["/etc/hosts"]}]}"#,
Some(r#"{"app.murky":[],"app.writes":[]}"#),
);
const FIRED: &str = "FIRED on evidence this report carries";
const REFUSING: &str = "Refusing (exit 2)";
let (rc, _, err) = run_gate(&loc, &pol(&f.dir, "sole", "deny Unknown[unresolved] app.murky\n"), &[]);
assert_eq!(rc, 2, "{err}");
assert!(err.contains(REFUSING), "a refusal must name its own disposition:\n{err}");
assert!(
!err.contains(FIRED),
"THE FALSE CLAIM: no rule fired on carried evidence — the only rule in this policy is the \
unanswerable one:\n{err}"
);
let (rc, _, err) = run_gate(
&loc,
&pol(&f.dir, "both", "deny Fs app.writes\ndeny Unknown[unresolved] app.murky\n"),
&[],
);
assert_eq!(rc, 1, "{err}");
assert!(
err.contains("the 1 violation(s) reported below FIRED on evidence this report carries"),
"the claim must be COUNTED from the verdict, not asserted beside it:\n{err}"
);
assert!(
!err.contains(REFUSING),
"THE OTHER FALSE CLAIM: this run exits 1, so no line of it may announce exit 2:\n{err}"
);
assert!(err.contains("deny Unknown[unresolved] app.murky"), "{err}");
let (rc, _, err) = run_gate(&loc, &pol(&f.dir, "clean", "deny Exec app\n"), &[]);
assert_eq!(rc, 0, "{err}");
assert!(!err.contains(FIRED) && !err.contains(REFUSING), "{err}");
}
#[test]
fn gate_report_withholds_an_unevidenced_scoped_rule_instead_of_fabricating_a_violation() {
let f = Fixture::new("gate-withhold");
let loc = gate_fixture(
&f.dir,
"r",
r#"{"candor":{"version":"handwritten","spec":"0.24"},"package":"app",
"analyzed":{"count":2,"digest":"0"},
"functions":[
{"fn":"app.opaque","inferred":["Unknown"]},
{"fn":"app.writes","inferred":["Fs"],"direct":["Fs"],"paths":["/etc/hosts"]}]}"#,
Some(r#"{"app.opaque":[],"app.writes":[]}"#),
);
let doc_of = |name: &str, text: &str| -> (i32, String, serde_json::Value) {
let out = f.dir.join(format!("{name}.verdict.json"));
let _ = std::fs::remove_file(&out);
let (rc, _, err) =
run_gate(&loc, &pol(&f.dir, name, text), &["--gate-json", &out.to_string_lossy()]);
let v = std::fs::read_to_string(&out)
.ok()
.and_then(|d| serde_json::from_str::<serde_json::Value>(&d).ok())
.unwrap_or(serde_json::Value::Null);
(rc, err, v)
};
let (rc, err, v) = doc_of("scoped", "deny Unknown[unresolved] app.opaque\n");
assert_eq!(rc, 2, "a rule with no evidence to fire on must be WITHHELD, not charged:\n{err}");
assert_eq!(v["refused"], true, "…and a sole withholding is the refusal posture:\n{v}");
assert!(
v["violations"].as_array().map(|a| a.is_empty()).unwrap_or(true),
"A VIOLATION HERE IS A FABRICATION — `app.opaque`'s determinable class set is empty, so this \
record asserts a reason nobody recorded:\n{v}"
);
let (rc, err, v) = doc_of("bare", "deny Unknown app.opaque\n");
assert_eq!(rc, 1, "the bare rule must FIRE — else row A proves nothing:\n{err}");
assert_eq!(v["violations"][0]["fn"], "app.opaque", "{v}");
let (rc, err, v) = doc_of("both", "deny Fs app.writes\ndeny Unknown[unresolved] app.opaque\n");
assert_eq!(rc, 1, "the certain violation dominates the withholding:\n{err}");
let fns: Vec<&str> =
v["violations"].as_array().unwrap().iter().map(|x| x["fn"].as_str().unwrap()).collect();
assert_eq!(
fns,
vec!["app.writes"],
"EXACTLY the evidenced violation, and exactly one: `app.writes` must be present (the `8b97e5c` \
property) and `app.opaque` absent (the `5a8cf48` property):\n{v}"
);
assert!(
err.contains("deny Unknown[unresolved] app.opaque"),
"…and the WITHHELD rule is still disclosed — withholding it silently is the mirror defect:\n{err}"
);
let mloc = gate_fixture(
&f.dir,
"m",
r#"{"candor":{"version":"handwritten","spec":"0.24"},"package":"app",
"analyzed":{"count":3,"digest":"0"},
"functions":[
{"fn":"app.inherits","inferred":["Unknown"],"calls":["app.src"]},
{"fn":"app.src","inferred":["Unknown"],"direct":["Unknown"]},
{"fn":"app.opaque","inferred":["Unknown"]}]}"#,
Some(r#"{"app.inherits":["app.src"],"app.src":[],"app.opaque":[]}"#),
);
let out = f.dir.join("mirror.verdict.json");
let _ = std::fs::remove_file(&out);
let (rc, _, err) = run_gate(
&mloc,
&pol(&f.dir, "mirror", "deny Unknown[unresolved] app\n"),
&["--gate-json", &out.to_string_lossy()],
);
assert_eq!(rc, 1, "AN EVIDENCED `unresolved` MUST STILL FIRE — this is the under-report mirror:\n{err}");
let v: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&out).unwrap()).unwrap();
let hits: Vec<&str> =
v["violations"].as_array().unwrap().iter().map(|x| x["fn"].as_str().unwrap()).collect();
assert_eq!(
hits,
vec!["app.inherits", "app.src"],
"the direct reasonless Unknown AND the caller inheriting it, and ONLY those:\n{v}"
);
for gv in v["violations"].as_array().unwrap() {
assert_eq!(
gv["reasonClass"][0], "unresolved",
"a charged Unknown must carry the class that evidenced the charge — the fabricated record \
carried none, and that absence is the tell:\n{gv}"
);
}
}
#[test]
fn a_refusal_overwrites_a_stale_verdict_and_makes_no_claim_about_violations() {
let f = Fixture::new("gate-refusal-doc");
let loc = gate_fixture(
&f.dir,
"r",
r#"{"candor":{"version":"handwritten","spec":"0.23"},"package":"app",
"analyzed":{"count":1,"digest":"0"},
"functions":[{"fn":"app.netNoClass","inferred":["Net"],"direct":["Net"],
"hosts":["h.example.com"]}]}"#,
None,
);
for (name, text) in [
("scoped", "deny Net[unknown-host] app.netNoClass\n"),
("forbidr", "forbid app -> dep\n"),
("allowr", "allow Net example.com\n"),
] {
let v = f.dir.join(format!("v-{name}.json"));
std::fs::write(&v, "{\n \"spec\": \"0.24\",\n \"ok\": true,\n \"violations\": []\n}\n").unwrap();
let (rc, _, err) = run_gate(&loc, &pol(&f.dir, name, text), &["--gate-json", &v.to_string_lossy()]);
assert_eq!(rc, 2, "{name} must still refuse:\n{err}");
let doc = std::fs::read_to_string(&v).expect("the path still exists");
let d: serde_json::Value = serde_json::from_str(&doc).unwrap();
assert_eq!(
d["ok"], false,
"{name}: a consumer keying ONLY on `ok` must land on FAIL — it read yesterday's `true`:\n{doc}"
);
assert_eq!(d["refused"], true, "{name}: …and one keying on `refused` learns why:\n{doc}");
assert!(
d.get("violations").is_none(),
"{name}: `violations` must be ABSENT, not empty — an empty array is exactly the claim a \
refusal cannot make, and every consumer reads it as 'we looked and found none':\n{doc}"
);
assert!(
d["reason"].as_str().is_some_and(|s| !s.is_empty()),
"{name}: the document must carry the refusal reason:\n{doc}"
);
}
let v = f.dir.join("v-ok.json");
let (rc, _, err) = run_gate(&loc, &pol(&f.dir, "bare", "deny Net\n"), &["--gate-json", &v.to_string_lossy()]);
assert_eq!(rc, 1, "the answerable control must FIRE:\n{err}");
let d: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&v).unwrap()).unwrap();
assert!(d.get("refused").is_none(), "a real verdict is not a refusal: {d:#}");
assert_eq!(d["violations"].as_array().unwrap().len(), 1, "{d:#}");
}
#[test]
fn the_config_that_supplied_the_vocabulary_is_named_on_the_verdict() {
let f = Fixture::new("gate-vocab");
let loc = gate_fixture(
&f.dir,
"r",
r#"{"candor":{"version":"handwritten","spec":"0.23"},"package":"app",
"analyzed":{"count":1,"digest":"0"},
"functions":[{"fn":"app.viaFfi","inferred":["Unknown"],"direct":["Unknown"],
"unknownWhy":["native:libc::open"]}]}"#,
None,
);
let home = f.dir.join("polhome");
std::fs::create_dir_all(home.join("rules")).unwrap();
std::fs::create_dir_all(home.join(".candor")).unwrap();
let cfgpath = home.join(".candor/config");
std::fs::write(&cfgpath, "unknown-alias corp = native\n").unwrap();
let p = pol(&home.join("rules"), "org", "deny Unknown[corp]\n");
let v = f.dir.join("verdict.json");
let (rc, _, err) = run_gate(&loc, &p, &["--gate-json", &v.to_string_lossy()]);
assert_eq!(rc, 1, "the alias resolves from a config ABOVE the policy:\n{err}");
let j: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&v).unwrap()).unwrap();
let named = j["policyVocabulary"]["config"].as_str().map(|s| std::fs::canonicalize(s).ok()).unwrap_or(None);
assert_eq!(
named,
std::fs::canonicalize(&cfgpath).ok(),
"a verdict changed by a file the operator cannot see NAMED is ambient input:\n{j:#}"
);
assert_eq!(
j["policyVocabulary"]["aliases"],
serde_json::json!({"corp": ["native"]}),
"the alias's DEFINITION is what moved the verdict, so it travels with the name:\n{j:#}"
);
assert!(j.get("vocabulary").is_none(), "the pre-`b4e9155` key must not survive beside it:\n{j:#}");
std::fs::write(&cfgpath, "unknown-alias corp = reflect\n").unwrap();
let v2 = f.dir.join("verdict2.json");
let (rc2, _, err2) = run_gate(&loc, &p, &["--gate-json", &v2.to_string_lossy()]);
assert_eq!(rc2, 0, "the alias narrows the rule:\n{err2}");
let bare = pol(&home.join("rules"), "bare", "deny Unknown\n");
let v3 = f.dir.join("verdict3.json");
let (rc3, _, _) = run_gate(&loc, &bare, &["--gate-json", &v3.to_string_lossy()]);
assert_eq!(rc3, 1);
let j3: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&v3).unwrap()).unwrap();
assert!(j3.get("policyVocabulary").is_none(), "an alias the policy never mentions is not disclosed:\n{j3:#}");
}
#[test]
fn the_vocabulary_disclosure_names_what_the_alias_expanded_to_not_merely_that_one_was_used() {
let f = Fixture::new("gate-vocab-value");
let loc = gate_fixture(
&f.dir,
"r",
r#"{"candor":{"version":"handwritten","spec":"0.23"},"package":"app",
"analyzed":{"count":2,"digest":"0"},
"functions":[{"fn":"app.viaFfi","inferred":["Unknown"],"direct":["Unknown"],
"unknownWhy":["native:libc::open"]},
{"fn":"app.viaRefl","inferred":["Unknown"],"direct":["Unknown"],
"unknownWhy":["reflect:Any::downcast"]}]}"#,
None,
);
let home = f.dir.join("polhome");
std::fs::create_dir_all(home.join("rules")).unwrap();
std::fs::create_dir_all(home.join(".candor")).unwrap();
let cfgpath = home.join(".candor/config");
let p = pol(&home.join("rules"), "org", "deny Unknown[corp]\n");
let vocab_of = |cfg: &str, out: &str| -> (i32, serde_json::Value) {
std::fs::write(&cfgpath, cfg).unwrap();
let v = f.dir.join(out);
let (rc, _, err) = run_gate(&loc, &p, &["--gate-json", &v.to_string_lossy()]);
let j: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(&v).unwrap_or_else(|e| panic!("no verdict at {out} ({e}):\n{err}")),
)
.unwrap();
(rc, j)
};
let (rc_narrow, narrow) = vocab_of("unknown-alias corp = reflect\n", "narrow.json");
let (rc_wide, wide) = vocab_of("unknown-alias corp = reflect,native\n", "wide.json");
assert_eq!(rc_narrow, 1, "the narrow definition catches the reflect hole:\n{narrow:#}");
assert_eq!(rc_wide, 1, "the wide definition catches both:\n{wide:#}");
assert_eq!(narrow["violations"].as_array().unwrap().len(), 1, "{narrow:#}");
assert_eq!(wide["violations"].as_array().unwrap().len(), 2, "{wide:#}");
assert_ne!(
narrow["policyVocabulary"]["aliases"], wide["policyVocabulary"]["aliases"],
"two configs that gate DIFFERENTLY under one unchanged policy line must not produce the same \
vocabulary disclosure — that is the whole of `7f5b5ba`:\nnarrow {narrow:#}\nwide {wide:#}"
);
assert_eq!(
narrow["policyVocabulary"]["aliases"],
serde_json::json!({"corp": ["reflect"]}),
"{narrow:#}"
);
assert_eq!(
wide["policyVocabulary"]["aliases"],
serde_json::json!({"corp": ["native", "reflect"]}),
"the classes are sorted, so the document is deterministic across runs:\n{wide:#}"
);
for (label, j) in [("narrow", &narrow), ("wide", &wide)] {
let named =
j["policyVocabulary"]["config"].as_str().map(|s| std::fs::canonicalize(s).ok()).unwrap_or(None);
assert_eq!(
named,
std::fs::canonicalize(&cfgpath).ok(),
"{label}: the config path must survive the shape change:\n{j:#}"
);
let keys: Vec<&String> =
j["policyVocabulary"]["aliases"].as_object().expect("an OBJECT").keys().collect();
assert_eq!(keys, vec!["corp"], "{label}: the keys ARE the old array — the names are not lost:\n{j:#}");
}
}
#[test]
fn gate_report_refuses_an_unrecognised_reason_class_token_including_beside_valid_ones() {
let f = Fixture::new("gate-badclass");
let loc = gate_fixture(
&f.dir,
"r",
r#"{"candor":{"version":"handwritten","spec":"0.23"},"package":"app",
"analyzed":{"count":1,"digest":"0"},
"functions":[{"fn":"app.viaFfi","inferred":["Unknown"],"direct":["Unknown"],
"unknownWhy":["native:libc::open"]}]}"#,
None,
);
let (rc, _, err) = run_gate(&loc, &pol(&f.dir, "good", "deny Unknown[dispatch,native]\n"), &[]);
assert_eq!(rc, 1, "the correctly-spelled rule must FIRE, or the rows below prove nothing:\n{err}");
for (name, text, token) in [
("typo_beside_valid", "deny Unknown[dispatch,nativ]\n", "nativ"),
("sole_unrecognised", "deny Unknown[corp]\n", "corp"),
] {
let (rc, stdout, err) = run_gate(&loc, &pol(&f.dir, name, text), &[]);
assert_eq!(
rc, 2,
"{name}: a policy that cannot be honoured AS WRITTEN must be refused, never silently \
rewritten into a different policy:\n{err}"
);
assert!(err.contains(token), "{name}: the refusal must NAME the token:\n{err}");
assert!(
err.contains("unresolved") && err.contains("dispatch"),
"{name}: …and list the ACCEPTED set, which is the only thing that makes it fixable:\n{err}"
);
assert!(stdout.is_empty(), "{name}: a refused policy produces no verdict");
}
let sub = f.dir.join("aliased");
std::fs::create_dir_all(sub.join(".candor")).unwrap();
std::fs::write(sub.join(".candor/config"), "unknown-alias corp = native\n").unwrap();
let p = pol(&sub, "alias", "deny Unknown[corp]\n");
let (rc, _, err) = run_gate(&loc, &p, &[]);
assert_eq!(rc, 1, "an alias DEFINED beside the policy resolves and the rule fires:\n{err}");
std::fs::write(sub.join(".candor/config"), "unknown-alias corp = dispatch,nativ\n").unwrap();
let (rc, stdout, err) = run_gate(&loc, &p, &[]);
assert_eq!(rc, 2, "a typo in the ALIAS DEFINITION must refuse, not narrow the definition:\n{err}");
assert!(err.contains("nativ"), "the refusal must name the token in the CONFIG:\n{err}");
assert!(stdout.is_empty());
std::fs::write(sub.join(".candor/config"), "unknown-alias unused = dispatch,nativ\n").unwrap();
let bare_p = pol(&sub, "barealias", "deny Unknown\n");
let (rc, _, err) = run_gate(&loc, &bare_p, &[]);
assert_eq!(rc, 1, "a typo'd alias the policy never mentions must not refuse the gate:\n{err}");
}
#[test]
fn gate_report_refuses_an_unrecognised_net_destination_class_token() {
let f = Fixture::new("gate-badnetclass");
let loc = gate_fixture(
&f.dir,
"r",
r#"{"candor":{"version":"handwritten","spec":"0.24"},"package":"app",
"analyzed":{"count":1,"digest":"0"},
"functions":[{"fn":"app.egress","inferred":["Net"],"direct":["Net"],
"hosts":["h.example.com"],"netClass":["unknown-host"]}]}"#,
None,
);
let (rc, _, err) = run_gate(&loc, &pol(&f.dir, "good", "deny Net[known-telemetry,unknown-host]\n"), &[]);
assert_eq!(rc, 1, "the correctly-spelled rule must FIRE, or the rows below prove nothing:\n{err}");
for (name, text, token) in [
("typo_beside_valid", "deny Net[known-telemetry,unknown-hosst]\n", "unknown-hosst"),
("sole_unrecognised", "deny Net[nope]\n", "nope"),
] {
let (rc, stdout, err) = run_gate(&loc, &pol(&f.dir, name, text), &[]);
assert_eq!(rc, 2, "{name}: a policy that cannot be honoured AS WRITTEN must be refused:\n{err}");
assert!(err.contains(token), "{name}: the refusal must NAME the token:\n{err}");
assert!(
err.contains("unknown-host") && err.contains("known-partner"),
"{name}: …and list the ACCEPTED set, which is the only thing that makes it fixable:\n{err}"
);
assert!(stdout.is_empty(), "{name}: a refused policy produces no verdict");
}
let (rc, _, err) = run_gate(&loc, &pol(&f.dir, "star", "deny Net[*]\n"), &[]);
assert_eq!(rc, 1, "`Net[*]` means every destination and must still evaluate:\n{err}");
}
#[test]
fn gate_report_answers_the_contributes_counterexample_rather_than_refusing_it() {
let f = Fixture::new("gate-contributes");
let loc = gate_fixture(
&f.dir,
"r",
r#"{"candor":{"version":"handwritten","spec":"0.23"},"package":"app",
"analyzed":{"count":3,"digest":"0"},
"functions":[
{"fn":"app.reasonless","inferred":["Unknown"],"direct":["Unknown"]},
{"fn":"app.reasoned","inferred":["Unknown"],"direct":["Unknown"],
"unknownWhy":["dispatch:app::Trait"]},
{"fn":"app.both","inferred":["Unknown"],"calls":["app.reasonless","app.reasoned"]}]}"#,
None,
);
let fired = |name: &str, text: &str| -> (i32, Vec<String>) {
let p = pol(&f.dir, name, text);
let out = f.dir.join(format!("{name}.verdict.json"));
let _ = std::fs::remove_file(&out);
let (rc, _, err) = run_gate(&loc, &p, &["--gate-json", &out.to_string_lossy()]);
assert!(rc != 2, "`{text}` must be ANSWERED, not refused (exit 2):\n{err}");
let v: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&out).expect("a verdict")).unwrap();
let mut fns: Vec<String> = v["violations"]
.as_array()
.unwrap()
.iter()
.map(|x| x["fn"].as_str().unwrap().to_string())
.collect();
fns.sort();
(rc, fns)
};
let (rc, fns) = fired("unres", "deny Unknown[unresolved]\n");
assert_eq!(rc, 1);
assert_eq!(
fns,
vec!["app.both".to_string(), "app.reasonless".to_string()],
"the reasonless DIRECT Unknown contributes `unresolved` AT THE ENTRY, so it composes: the caller \
of BOTH a reasonless and a reasoned dep is caught too — the §6.2 counterexample in which adding \
a call turned a red verdict green"
);
let (_, fns) = fired("disp", "deny Unknown[dispatch]\n");
assert_eq!(
fns,
vec!["app.both".to_string(), "app.reasoned".to_string()],
"a named direct Unknown keeps ONLY its own class — the naive unconditional contribution would \
put `app.reasonless` here too"
);
}
#[test]
fn gate_report_json_is_gate_json_dash_and_stdout_stays_parseable() {
let f = Fixture::new("gate-json");
let loc = gate_fixture(
&f.dir,
"r",
r#"{"candor":{"version":"handwritten","spec":"0.23"},"package":"app",
"analyzed":{"count":2,"digest":"0"},
"coverage":{"uncovered":[{"name":"zeta","calls":2},{"name":"alpha","calls":1}]},
"functions":[{"fn":"app.egress","inferred":["Net"],"direct":["Net"],
"hosts":["example.com"],"netClass":["unknown-host"]}]}"#,
None,
);
let p = pol(&f.dir, "denynet", "deny Net\n");
let file = f.dir.join("verdict.json");
let (rc_json, stdout, err) = run_gate(&loc, &p, &["--json"]);
assert_eq!(rc_json, 1);
let streamed: serde_json::Value =
serde_json::from_str(stdout.trim()).unwrap_or_else(|e| panic!("stdout must be pure JSON ({e}): {stdout}\n{err}"));
assert!(err.contains("AS-EFF-006"), "the prose belongs on stderr:\n{err}");
let (rc_file, stdout2, _) = run_gate(&loc, &p, &["--gate-json", &file.to_string_lossy()]);
assert_eq!(rc_file, 1);
let written: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap();
assert_eq!(streamed, written, "`--json` and `--gate-json <file>` must be the same document");
assert!(stdout2.contains("AS-EFF-006"), "without a stdout JSON document the prose goes to stdout");
assert_eq!(streamed["analyzed"]["count"], 2);
assert_eq!(streamed["coverage"]["uncovered"], 2);
assert_eq!(streamed["coverage"]["packages"], serde_json::json!(["alpha", "zeta"]));
assert_eq!(streamed["spec"], candor_report_spec());
}
fn candor_report_spec() -> &'static str {
candor_report::SPEC_VERSION
}
#[test]
fn gate_report_will_not_certify_over_a_report_that_declares_itself_incomplete() {
let f = Fixture::new("gate-incomplete");
let loc = gate_fixture(
&f.dir,
"r",
r#"{"candor":{"version":"handwritten","spec":"0.23"},"package":"app",
"analyzed":{"count":1,"digest":"0"},
"unanalyzed":[{"path":"src/bad.rs","reason":"source failed to parse"}],
"functions":[{"fn":"app.pure_enough","inferred":[]}]}"#,
None,
);
let file = f.dir.join("verdict.json");
let (rc, _, err) = run_gate(&loc, &pol(&f.dir, "denyfs", "deny Fs\n"), &["--gate-json", &file.to_string_lossy()]);
assert_eq!(rc, 2, "a gate cannot be green over code candor never analyzed:\n{err}");
let v: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap();
assert_eq!(v["ok"], false);
assert_eq!(v["incomplete"], true);
assert_eq!(v["unanalyzed"][0]["path"], "src/bad.rs");
}
#[test]
fn an_incomplete_report_does_not_swallow_the_violations_it_also_carries() {
let f = Fixture::new("gate-incomplete-viol");
let loc = gate_fixture(
&f.dir,
"r",
r#"{"candor":{"version":"handwritten","spec":"0.23"},"package":"app",
"analyzed":{"count":9,"digest":"0"},
"unanalyzed":[{"path":"src/bad.rs","reason":"source failed to parse"}],
"functions":[{"fn":"app.netOne","inferred":["Net"],"direct":["Net"],"hosts":["a.example.com"]},
{"fn":"app.netTwo","inferred":["Net"],"direct":["Net"],"hosts":["b.example.com"]},
{"fn":"app.pure_enough","inferred":[]}]}"#,
None,
);
let file = f.dir.join("verdict.json");
let (rc, _, err) =
run_gate(&loc, &pol(&f.dir, "denynet", "deny Net\n"), &["--gate-json", &file.to_string_lossy()]);
let v: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap();
let fns: Vec<&str> =
v["violations"].as_array().unwrap().iter().filter_map(|x| x["fn"].as_str()).collect();
assert_eq!(
fns,
vec!["app.netOne", "app.netTwo"],
"an incomplete manifest must not delete the findings from the verdict a CI consumer reads \
(SPEC §3.3 — a real violation dominates):\n{v:#}\nstderr:\n{err}"
);
assert_eq!(v["ok"], false, "{v:#}");
assert_eq!(v["incomplete"], true, "the manifest still rides the verdict:\n{v:#}");
assert_eq!(v["unanalyzed"][0]["path"], "src/bad.rs", "{v:#}");
assert_eq!(rc, 1, "a real violation dominates the incomplete exit 2:\n{err}");
}
#[test]
fn gate_report_discloses_a_report_that_judged_nothing_without_moving_the_verdict() {
let f = Fixture::new("gate-judged-nothing");
let p = pol(&f.dir, "denyfs", "deny Fs\n");
let body = |count: &str| format!(
r#"{{"candor":{{"version":"handwritten","spec":"0.24"}},"package":"app",
"analyzed":{{"count":{count},"digest":"0"}},
"functions":[]}}"#);
let zero = gate_fixture(&f.dir, "zero", &body("0"), None);
let vfile = f.dir.join("verdict-zero.json");
let (rc, _, err) = run_gate(&zero, &p, &["--gate-json", &vfile.to_string_lossy()]);
assert!(err.contains("JUDGED NOTHING") && err.contains("analyzed.count"),
"the verb MUST say the report judged nothing — that disclosure IS the obligation, and \
deleting the old refusal without it is the defect wearing the fix's clothes:\n{err}");
assert!(err.contains("`app`"),
"…and must NAME THE PACKAGE, or an adopter chaining twenty reports cannot act on it:\n{err}");
assert_eq!(rc, 0,
"the exit code is UNCHANGED (⟨0.24⟩): §3.3 has exactly two exit-2 causes and a \
judged-nothing dependency is neither, so refusing here splits the verb:\n{err}");
let v: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(&vfile).expect("the verdict document is UNCHANGED too — §3.1 byte-equality \
with `scan --policy`, which writes one and exits 0")).unwrap();
assert_eq!(v["ok"], true, "{v:#}");
assert_eq!(v["analyzed"]["count"], 0, "the count-0 manifest rides the verdict verbatim: {v:#}");
assert_eq!(v["violations"].as_array().unwrap().len(), 0, "{v:#}");
let allpure = gate_fixture(&f.dir, "allpure", &body("2"), None);
let vfile = f.dir.join("verdict-allpure.json");
let (rc, _, err) = run_gate(&allpure, &p, &["--gate-json", &vfile.to_string_lossy()]);
assert_eq!(rc, 0, "an all-pure package's report is a CLAIM (§2 rule 3) and must still gate clean:\n{err}");
assert!(!err.contains("JUDGED NOTHING"), "…with no ⟨0.24⟩ hedge anywhere:\n{err}");
let v: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&vfile).unwrap()).unwrap();
assert_eq!(v["ok"], true);
assert_eq!(v["violations"].as_array().unwrap().len(), 0);
assert!(v.get("incomplete").is_none(), "a believed all-pure verdict carries no new hedge: {v}");
let legacy = gate_fixture(&f.dir, "legacy",
r#"{"candor":{"version":"handwritten","spec":"0.20"},"package":"app","functions":[]}"#, None);
let (rc, _, err) = run_gate(&legacy, &p, &[]);
assert_eq!(rc, 0, "a manifest-less EMPTY report is disclosed, not refused (⟨0.24⟩ corrected):\n{err}");
assert!(err.contains("JUDGED NOTHING"), "…but it IS disclosed:\n{err}");
let legacy_full = gate_fixture(&f.dir, "legacyfull",
r#"{"candor":{"version":"handwritten","spec":"0.20"},"package":"app",
"functions":[{"fn":"app.pure_enough","inferred":[]}]}"#, None);
let (rc, _, err) = run_gate(&legacy_full, &p, &[]);
assert_eq!(rc, 0,
"a pre-⟨0.21⟩ report that LISTS entries judged something — refusing it would withdraw \
every manifest-less report from the verb, which is the emptiness fix wearing a \
different hat:\n{err}");
assert!(!err.contains("JUDGED NOTHING"), "…and it earns no hedge:\n{err}");
let contra = gate_fixture(&f.dir, "contra",
r#"{"candor":{"version":"handwritten","spec":"0.24"},"package":"app",
"analyzed":{"count":0,"digest":"0"},
"functions":[{"fn":"app.reads","inferred":["Fs"],"direct":["Fs"],"paths":["/etc/x"]}]}"#, None);
let (rc, _, err) = run_gate(&contra, &p, &[]);
assert_eq!(rc, 1, "the gate still evaluates what the report DOES carry:\n{err}");
assert!(err.contains("JUDGED NOTHING"), "…and still discloses the contradiction:\n{err}");
}
#[test]
fn a_present_but_unparseable_section2_key_refuses_and_an_absent_one_does_not() {
let f = Fixture::new("gate-corrupt-key");
let p = pol(&f.dir, "denynet", "deny Net\n");
let body = |extra: &str| format!(
r#"{{"candor":{{"version":"handwritten","spec":"0.24"}},"package":"app",
"analyzed":{{"count":3,"digest":"0"}}{extra},
"functions":[{{"fn":"app.pure_enough","inferred":[]}}]}}"#);
for (name, extra) in [
("wrongfields", r#","unanalyzed":[{"unit":"src/broken.rs","why":"parse error"}]"#),
("barestrings", r#","unanalyzed":["src/broken.rs"]"#),
("notalist", r#","unanalyzed":"src/broken.rs""#),
] {
let loc = gate_fixture(&f.dir, name, &body(extra), None);
let vfile = f.dir.join(format!("v-{name}.json"));
let _ = std::fs::remove_file(&vfile);
let (rc, _out, err) = run_gate(&loc, &p, &["--gate-json", &vfile.to_string_lossy()]);
assert_eq!(rc, 2, "a present-but-unparseable `unanalyzed` must refuse, not read as `[]`:\n{err}");
assert!(err.contains("`unanalyzed`"), "the refusal must NAME the key it could not read:\n{err}");
let v: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&vfile).expect("a refusal document")).unwrap();
assert_eq!(v["ok"], false, "the naive read of this document must be the fail-closed one:\n{v:#}");
assert_eq!(v["refused"], true, "{v:#}");
assert!(v.get("violations").is_none(), "a refusal makes NO claim about violations:\n{v:#}");
}
for (name, extra) in [
("boolcount", r#""analyzed":{"count":true},"#),
("strmanifest", r#""analyzed":"lots","#),
] {
let rep = format!(
r#"{{"candor":{{"version":"handwritten","spec":"0.24"}},"package":"app",{extra}
"functions":[{{"fn":"app.pure_enough","inferred":[]}}]}}"#);
let loc = gate_fixture(&f.dir, name, &rep, None);
let (rc, _, err) = run_gate(&loc, &p, &[]);
assert_eq!(rc, 2, "a manifest that cannot be read is not a manifest:\n{err}");
assert!(err.contains("`analyzed`"), "the refusal must NAME the key:\n{err}");
}
for (name, extra) in [("absent", ""), ("emptylist", r#","unanalyzed":[]"#)] {
let loc = gate_fixture(&f.dir, name, &body(extra), None);
let vfile = f.dir.join(format!("v-{name}.json"));
let (rc, _, err) = run_gate(&loc, &p, &["--gate-json", &vfile.to_string_lossy()]);
assert_eq!(rc, 0, "an {name} `unanalyzed` is the documented default, never a refusal:\n{err}");
let v: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&vfile).unwrap()).unwrap();
assert_eq!(v["ok"], true, "{v:#}");
assert!(v.get("incomplete").is_none(), "…and no incompleteness is invented either: {v:#}");
}
let loc = gate_fixture(&f.dir, "nodigest",
r#"{"candor":{"version":"handwritten","spec":"0.24"},"package":"app","analyzed":{"count":5},
"functions":[{"fn":"app.pure_enough","inferred":[]}]}"#, None);
let vfile = f.dir.join("v-nodigest.json");
let (rc, _, err) = run_gate(&loc, &p, &["--gate-json", &vfile.to_string_lossy()]);
assert_eq!(rc, 0, "a digest-less manifest is legible, not corrupt:\n{err}");
let v: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&vfile).unwrap()).unwrap();
assert_eq!(v["analyzed"]["count"], 5, "the judged count must ride the verdict, not be zeroed: {v:#}");
}
#[test]
fn gate_report_grammar_is_loud() {
let f = Fixture::new("gate-grammar");
let loc = gate_fixture(
&f.dir,
"r",
r#"{"candor":{"version":"handwritten","spec":"0.23"},"package":"app",
"analyzed":{"count":1,"digest":"0"},
"functions":[{"fn":"app.egress","inferred":["Net"],"direct":["Net"],
"hosts":["e.com"],"netClass":["unknown-host"]}]}"#,
None,
);
let p = pol(&f.dir, "denynet", "deny Net\n");
let ps = p.to_string_lossy().into_owned();
let missing = f.dir.join("nope.policy").to_string_lossy().into_owned();
for (args, needle) in [
(vec!["gate", "--report", &loc, "--policy", &ps, "stray"], "unexpected argument"),
(vec!["gate", "--report", &loc, "--policy", &ps, "--nope"], "unknown flag"),
(vec!["gate", "--report", &loc], "a policy is required"),
(vec!["gate", "--report", &loc, "--policy", &missing], "could not be read"),
(vec!["gate", "--report", &loc, "--policy"], "--policy requires"),
(vec!["gate", "--policy", &ps, "--report"], "--report requires"),
(vec!["gate", "--report", &loc, "--gate-json", "--policy", &ps], "--gate-json requires"),
] {
let out = Command::new(bin()).args(&args).output().expect("run candor-query");
let err = String::from_utf8_lossy(&out.stderr).into_owned();
assert_eq!(out.status.code(), Some(2), "{args:?} must be a loud usage error:\n{err}");
assert!(err.contains(needle), "{args:?} must say `{needle}`, got:\n{err}");
}
let nowhere = f.dir.join("nothing-here").to_string_lossy().into_owned();
let (rc, _, err) = run_gate(&nowhere, &p, &[]);
assert_eq!(rc, 2, "an empty locator must not read as a clean gate:\n{err}");
let bad = gate_fixture(&f.dir, "corrupt", "{ not json at all", None);
let (rc, _, err) = run_gate(&bad, &p, &[]);
assert_eq!(rc, 2, "a corrupt report is corrupt input, not an effect-free package:\n{err}");
}
#[test]
fn gate_flag_shaped_policy_value_is_refused_and_the_swallowed_sink_still_gets_the_document() {
let f = Fixture::new("gate-b13");
let loc = gate_fixture(
&f.dir,
"r",
r#"{"candor":{"version":"handwritten","spec":"0.23"},"package":"app",
"analyzed":{"count":1,"digest":"0"},
"functions":[{"fn":"app.egress","inferred":["Net"],"direct":["Net"],
"hosts":["e.com"],"netClass":["unknown-host"]}]}"#,
None,
);
let run = |args: &[&str]| -> (Option<i32>, String, String) {
let out = Command::new(bin())
.args(args)
.env_remove("CANDOR_POLICY").env_remove("CANDOR_CONFIG").env_remove("CANDOR_REPORT")
.output().expect("run candor-query");
(out.status.code(),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned())
};
let (rc, stdout, stderr) = run(&["gate", "--report", &loc, "--policy", "--gate-json", "-"]);
assert_eq!(rc, Some(2), "a flag-shaped --policy value is a usage error:\n{stderr}");
let doc: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap_or_else(|_| {
panic!("the `--gate-json -` stream sink must carry the refusal document \
(it was swallowed as the policy filename), got stdout:\n{stdout}")
});
assert_eq!(doc["ok"], false, "fail-closed to a naive reader: {doc}");
assert_eq!(doc["refused"], true, "a refusal, not a verdict: {doc}");
assert!(stderr.contains("--policy") && stderr.contains("--gate-json"),
"stderr names the flag given no value AND the token that is not one: {stderr}");
let g = f.dir.join("verdict.json");
std::fs::write(&g, "{\"ok\": true}\n").unwrap();
let gs = g.to_string_lossy().into_owned();
let (rc, _, stderr) = run(&["gate", "--report", &loc, "--policy", "--gate-json", &gs]);
assert_eq!(rc, Some(2), "{stderr}");
let doc: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&g).expect("sink written")).expect("valid JSON");
assert_eq!(doc["ok"], false, "the stale green was replaced by the refusal: {doc}");
assert_eq!(doc["refused"], true, "{doc}");
assert!(doc["reason"].as_str().unwrap_or("").contains("--policy"),
"the document carries the usage cause, not the armed placeholder: {doc}");
let p = pol(&f.dir, "denyfs", "deny Fs\n");
let (rc, stdout, stderr) =
run(&["gate", "--report", &loc, "--policy", &p.to_string_lossy(), "--gate-json", "-"]);
assert_eq!(rc, Some(0), "deny Fs over a Net-only report passes:\n{stderr}");
let doc: serde_json::Value = serde_json::from_str(stdout.trim()).expect("the stream verdict");
assert_eq!(doc["ok"], true, "a real verdict, not a refusal: {doc}");
assert!(doc.get("refused").is_none(), "{doc}");
}
#[test]
fn query_verbs_refuse_a_flag_shaped_value_for_every_value_taking_flag() {
let f = Fixture::new("grammar-b13");
f.write_report();
let rp = f.report_path();
for (args, flag) in [
(vec!["where", "Fs", "--report", "--json"], "--report"),
(vec!["whatif", "outer", "Fs", "--report", rp.as_str(), "--policy", "--json"], "--policy"),
(vec!["blindspots", "--report", rp.as_str(), "--class", "--json"], "--class"),
] {
let out = Command::new(bin())
.args(&args)
.env_remove("CANDOR_POLICY").env_remove("CANDOR_CONFIG").env_remove("CANDOR_REPORT")
.output().expect("run candor-query");
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
assert_eq!(out.status.code(), Some(2), "{args:?} must be a usage error:\n{stderr}");
assert!(stderr.contains("given no value") && stderr.contains(flag) && stderr.contains("--json"),
"{args:?} must name {flag} AND the flag-shaped token, got:\n{stderr}");
}
let out = Command::new(bin())
.args(["where", "Fs", "--report", &f.report_path(), "--json"])
.env_remove("CANDOR_POLICY").env_remove("CANDOR_CONFIG").env_remove("CANDOR_REPORT")
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0), "a value-shaped --report is unaffected");
let v: serde_json::Value =
serde_json::from_str(String::from_utf8_lossy(&out.stdout).trim()).expect("--json reached the output mode");
assert!(v.is_object() || v.is_array(), "{v}");
}
#[test]
fn unverified_and_fix_gate_answer_the_narrowed_rule_the_gate_actually_applies() {
let f = Fixture::new("filter-aware");
let loc = gate_fixture(
&f.dir,
"r",
r#"{"candor":{"version":"handwritten","spec":"0.23"},"package":"app",
"analyzed":{"count":1,"digest":"0"},
"functions":[{"fn":"app.port","inferred":["Unknown"],"direct":["Unknown"],
"unknownWhy":["callback:injected port"]}]}"#,
None,
);
let run = |verb: &str, p: &std::path::Path| -> (i32, String) {
let out = Command::new(bin())
.args([verb, "--report", &loc, "--policy", &p.to_string_lossy(), "--json"])
.output()
.expect("run candor-query");
(out.status.code().unwrap_or(-1), String::from_utf8_lossy(&out.stdout).into_owned())
};
let json = |s: &str| -> serde_json::Value { serde_json::from_str(s).unwrap() };
let miss = pol(&f.dir, "miss", "deny Unknown[reflect] app\n");
let (rc, _, err) = run_gate(&loc, &miss, &[]);
assert_eq!(rc, 0, "the gate tolerates a class filter this entry does not match:\n{err}");
let (urc, uout) = run("unverified", &miss);
let u = json(&uout);
assert_eq!(urc, 0, "advisory");
assert_eq!(
u["unverified"].as_array().map(Vec::len),
Some(1),
"the gate DECLINED to clear `app.port` under this rule, so its purity is asserted and not \
verified — that is the disclosure's whole subject, and it was empty:\n{uout}"
);
assert_eq!(u["unverified"][0]["fn"], "app.port");
assert_eq!(
u["unverified"][0]["rule"], "deny Unknown[reflect] app",
"…and the rule is named WITH its filter — printing the operator's narrowed rule back as the \
wide one is the mis-attribution this fix must not manufacture:\n{uout}"
);
assert_eq!(u["unverified"][0]["upgrade"], "deny Unknown app", "widen the filter, not append a second Unknown");
let (frc, fout) = run("fix-gate", &miss);
assert_eq!(frc, 0);
assert_eq!(
json(&fout)["remedies"].as_array().map(Vec::len),
Some(0),
"the gate reports no crossing here, so there is no boundary to hoist across:\n{fout}"
);
let hit = pol(&f.dir, "hit", "deny Unknown[indirect] app\n");
let (rc2, _, err2) = run_gate(&loc, &hit, &[]);
assert_eq!(rc2, 1, "`callback:` classifies as `indirect`, so this filter FIRES:\n{err2}");
let (_, uout2) = run("unverified", &hit);
assert_eq!(
json(&uout2)["unverified"].as_array().map(Vec::len),
Some(0),
"a function the gate CHARGED is a violation, not an unverified pass:\n{uout2}"
);
let (_, fout2) = run("fix-gate", &hit);
assert_eq!(
json(&fout2)["remedies"].as_array().map(Vec::len),
Some(1),
"…and the crossing the gate DOES report still gets its remedy — the filter-aware fix must not \
turn `fix-gate` silent on the violations it exists for:\n{fout2}"
);
let bare = pol(&f.dir, "bare", "deny Unknown app\n");
let (rc3, _, _) = run_gate(&loc, &bare, &[]);
assert_eq!(rc3, 1);
let (_, uout3) = run("unverified", &bare);
assert_eq!(json(&uout3)["unverified"].as_array().map(Vec::len), Some(0), "a bare deny Unknown fires on every hole");
let pure = pol(&f.dir, "pure", "pure app\n");
let (_, uout4) = run("unverified", &pure);
let u4 = json(&uout4);
assert_eq!(u4["unverified"].as_array().map(Vec::len), Some(1));
assert_eq!(u4["unverified"][0]["upgrade"], "deny Unknown app", "PART 12c's four-way form, unmoved");
}
#[test]
fn whatif_names_the_operators_own_rule_and_discloses_what_a_narrowed_verdict_rests_on() {
let f = Fixture::new("whatif-raw");
let loc = gate_fixture(
&f.dir,
"r",
r#"{"candor":{"version":"handwritten","spec":"0.24"},"package":"app",
"analyzed":{"count":1,"digest":"0"},
"functions":[{"fn":"app.nat","inferred":["Unknown"],"direct":["Unknown"],
"unknownWhy":["native:extern fn"]}]}"#,
Some(r#"{"app.nat":[]}"#),
);
let whatif = |text: &str, effect: &str| -> serde_json::Value {
let p = pol(&f.dir, "w", text);
let out = Command::new(bin())
.args(["whatif", "app.nat", effect, "--report", &loc, "--policy", &p.to_string_lossy(), "--json"])
.output()
.expect("run candor-query");
serde_json::from_str(&String::from_utf8_lossy(&out.stdout)).unwrap()
};
let v = whatif("deny Net Db app # keep the app layer pure\n", "Net");
assert_eq!(v["ok"], false);
assert_eq!(
v["violations"][0]["rule"], "deny Net Db app",
"the operator's own line, not one rebuilt from the effect they happened to ask about:\n{v:#}"
);
let v = whatif("pure app\n", "Net");
assert_eq!(v["violations"][0]["rule"], "pure app", "a `pure` rule reads back as itself:\n{v:#}");
let v = whatif("deny Unknown[reflect] app.nat\n", "Unknown");
assert_eq!(v["violations"][0]["rule"], "deny Unknown[reflect] app.nat", "{v:#}");
let v2 = whatif("deny Net[unknown-host] app\n", "Net");
assert_eq!(v2["violations"][0]["rule"], "deny Net[unknown-host] app", "{v2:#}");
assert_eq!(
v["violations"][0]["conditional"], "the `Unknown` you introduce is of reason class reflect",
"a rule that NARROWS cannot be evaluated against an effect that does not exist yet — §3.1: \
disclose the unanswerable condition, never score it as a failed one:\n{v:#}"
);
assert_eq!(
v2["violations"][0]["conditional"], "the `Net` you introduce reaches destination class unknown-host",
"{v2:#}"
);
let plain = whatif("deny Unknown app.nat\n", "Unknown");
assert_eq!(plain["violations"][0]["rule"], "deny Unknown app.nat");
assert!(
plain["violations"][0].get("conditional").is_none(),
"a rule that does not narrow rests on no condition:\n{plain:#}"
);
let bare_net = whatif("deny Net app\n", "Net");
assert!(bare_net["violations"][0].get("conditional").is_none(), "{bare_net:#}");
let other = whatif("deny Net[unknown-host] Fs app\n", "Fs");
assert_eq!(other["violations"][0]["rule"], "deny Net[unknown-host] Fs app");
assert!(
other["violations"][0].get("conditional").is_none(),
"the `Net` filter says nothing about an introduced `Fs`:\n{other:#}"
);
}
#[test]
fn unevaluated_rides_the_gate_json_document_one_entry_per_rule() {
let f = Fixture::new("unevaldoc");
f.write_report();
let pol = f.dir.join("p.policy");
let verdict = f.dir.join("v.json");
let run = |policy: &str| -> (Option<i32>, String, serde_json::Value) {
std::fs::write(&pol, policy).unwrap();
let _ = std::fs::remove_file(&verdict); let out = Command::new(bin())
.args(["gate", "--report", &f.report_path(), "--policy"])
.arg(&pol)
.arg("--gate-json")
.arg(&verdict)
.env_remove("CANDOR_POLICY")
.env_remove("CANDOR_CONFIG")
.output()
.expect("run candor-query gate");
let err = String::from_utf8_lossy(&out.stderr).into_owned();
let doc = std::fs::read_to_string(&verdict)
.unwrap_or_else(|e| panic!("no --gate-json document ({e}); stderr was:\n{err}"));
(out.status.code(), err, serde_json::from_str(&doc).unwrap())
};
let rules_of = |v: &serde_json::Value| -> Vec<String> {
v["unevaluated"]
.as_array()
.map(|a| a.iter().map(|u| u["rule"].as_str().unwrap().to_string()).collect())
.unwrap_or_default()
};
let (rc, err, v) = run("deny Fs\n");
assert_eq!(rc, Some(1), "the control fires: {err}");
assert!(
v.get("unevaluated").is_none(),
"a fully-answered policy must stay byte-identical to a pre-rung verdict: {v}"
);
let (rc, err, v) = run("deny Fs\nallow Fs /var/data\n");
assert_eq!(rc, Some(1), "the certain violation still dominates (Lemma 2): {err}");
assert!(
v["violations"].as_array().is_some_and(|a| !a.is_empty()),
"the violation is not displaced by the disclosure: {v}"
);
assert_eq!(
rules_of(&v),
vec!["allow Fs /var/data".to_string()],
"the unanswered rule reaches the MACHINE channel, with the operator's own line verbatim: {v}"
);
assert!(
v["unevaluated"][0]["why"].as_str().is_some_and(|w| !w.is_empty()),
"…and says why: {v}"
);
let (rc, err, v) = run("deny Fs\nforbid app -> infra\nforbid web -> db\n");
assert_eq!(rc, Some(1), "still exit 1: {err}");
assert_eq!(
rules_of(&v),
vec!["forbid app -> infra".to_string(), "forbid web -> db".to_string()],
"TWO forbid lines are TWO entries — an aggregate answers `how many` when the question is \
`which`: {v}"
);
let (rc, err, v) = run("forbid app -> infra\nallow Fs /var/data\n");
assert_eq!(rc, Some(2), "a policy that is nothing but unanswerable rules refuses: {err}");
assert_eq!(v["refused"], serde_json::json!(true), "still a refusal document: {v}");
assert!(
v.get("violations").is_none(),
"MIRROR: a refusal carries no `violations` key — `[]` is the claim it cannot make: {v}"
);
assert_eq!(
rules_of(&v),
vec!["forbid app -> infra".to_string(), "allow Fs /var/data".to_string()],
"the sole refusal names every rule it could not decide: {v}"
);
assert!(
err.contains("`forbid app -> infra`") && err.contains("`allow Fs /var/data`"),
"stderr names the same rules the document does: {err}"
);
}
#[test]
fn parsepolicy_reports_every_line_it_did_not_honour() {
let f = Fixture::new("parsepolerr");
let pol = f.dir.join("p.policy");
let parse = |text: &str| -> serde_json::Value {
std::fs::write(&pol, text).unwrap();
let out = Command::new(bin())
.arg("parsepolicy")
.arg(&pol)
.env_remove("CANDOR_CONFIG")
.output()
.expect("run candor-query parsepolicy");
assert_eq!(
out.status.code(),
Some(0),
"§3.1: parsepolicy MUST NOT REFUSE a policy it can read and cannot honour — it REPORTS the \
parse. stderr:\n{}",
String::from_utf8_lossy(&out.stderr)
);
serde_json::from_slice(&out.stdout).expect("parsepolicy emits one JSON document")
};
let errs = |v: &serde_json::Value| -> Vec<(String, String, String)> {
v["errors"]
.as_array()
.map(|a| {
a.iter()
.map(|e| {
(
e["kind"].as_str().unwrap().to_string(),
e["token"].as_str().unwrap().to_string(),
e["rule"].as_str().unwrap().to_string(),
)
})
.collect()
})
.unwrap_or_default()
};
let v = parse("deny Fs\nallow Net github.com\nforbid app -> infra\npure core\n");
assert!(v.get("errors").is_none(), "a clean parse emits no `errors` key: {v}");
let v = parse(concat!(
"deny notaneffect\n", "allow Clock whatever\n", "forbid bad\n", "nonsense line\n", "allow Net in\n", "deny Fs Unknown[bogus,reflect] io\n", "deny Net[bogus,unknown-host] mixed\n", ));
let got = errs(&v);
assert_eq!(got.len(), 7, "EVERY not-honoured line is reported, not just the fatal ones: {v}");
let kinds: Vec<&str> = got.iter().map(|(k, _, _)| k.as_str()).collect();
assert_eq!(
kinds,
vec![
"effect-name",
"effect-name",
"rule-kind",
"rule-kind",
"rule-kind",
"reason-class/alias",
"Net destination-class",
],
"`kind` is drawn from §3.1's CLOSED set, in source order: {v}"
);
assert_eq!(got[0].2, "deny notaneffect");
assert_eq!(got[6].2, "deny Net[bogus,unknown-host] mixed");
assert_eq!(got[5].1, "bogus", "the token is the finding: {v}");
assert_eq!(got[3].1, "nonsense");
for e in v["errors"].as_array().unwrap() {
assert!(
e["accepted"].is_array(),
"`accepted` is an array of tokens: {e}"
);
}
assert_eq!(
v["errors"][5]["accepted"],
serde_json::json!(["reflect", "dispatch", "indirect", "native", "unresolved", "setup", "dynamic", "*"]),
"the reason-class vocabulary is named token by token: {v}"
);
let v = parse("deny Fs\nnonsense line\n");
assert_eq!(errs(&v).len(), 1, "the dropped line is reported: {v}");
assert_eq!(
v["deny"].as_array().map(Vec::len),
Some(1),
"…and the rest of the policy still parsed: {v}"
);
f.write_report();
std::fs::write(&pol, "deny Fs\nnonsense line\n").unwrap();
let out = Command::new(bin())
.args(["gate", "--report", &f.report_path(), "--policy"])
.arg(&pol)
.env_remove("CANDOR_POLICY")
.env_remove("CANDOR_CONFIG")
.output()
.expect("run candor-query gate");
assert_eq!(
out.status.code(),
Some(1),
"MIRROR: widening `errors` must not widen what REFUSES — `deny Fs` still fires beside a dropped \
line (exit 1, not 2). stderr:\n{}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn a_typod_effect_name_is_a_policy_error_and_the_ambiguous_middle_is_not() {
let f = Fixture::new("typoeffect");
f.write_report();
let pol = f.dir.join("p.policy");
let run = |verb: &[&str], policy: &str| -> (Option<i32>, String) {
std::fs::write(&pol, policy).unwrap();
let mut c = Command::new(bin());
c.args(verb).args(["--report", &f.report_path(), "--policy"]).arg(&pol);
c.env_remove("CANDOR_POLICY").env_remove("CANDOR_CONFIG");
let out = c.output().expect("run candor-query");
(out.status.code(), String::from_utf8_lossy(&out.stderr).into_owned())
};
for (policy, why) in [
("deny Nett app\n", "a deny whose effect list ends up EMPTY is malformed under either reading"),
("deny notaneffect\n", "…including when the typo is the only token"),
("deny\n", "…and when there is no effect token at all"),
("allow Nett host.example\n", "`allow`'s effect position is closed, with no scope reading"),
("allow Clock whatever\n", "…and a real effect that `allow` does not cover is the same case"),
] {
let (rc, err) = run(&["gate"], policy);
assert_eq!(rc, Some(2), "gate `{}`: {why}\n{err}", policy.trim());
assert!(
err.contains("policy error"),
"gate `{}` must SAY it is a policy error, not drop the rule in silence:\n{err}",
policy.trim()
);
let (rc, err) = run(&["whatif", "outer", "Net"], policy);
assert_eq!(rc, Some(2), "whatif `{}`: the pre-edit verb refuses it too\n{err}", policy.trim());
}
let (rc, err) = run(&["gate"], "deny Net Exex app\n");
assert_eq!(rc, Some(0), "the ambiguous middle is NOT refused — `Exex` reads as a scope:\n{err}");
std::fs::write(&pol, "deny Net Exex app\n").unwrap();
let out = Command::new(bin())
.arg("parsepolicy")
.arg(&pol)
.env_remove("CANDOR_CONFIG")
.output()
.expect("run parsepolicy");
let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(v["deny"][0]["scope"], serde_json::json!("Exex"), "the scope reading is shown: {v}");
let (rc, err) = run(&["gate"], "deny Fs\n");
assert_eq!(rc, Some(1), "a well-formed policy still gates:\n{err}");
let (rc, err) = run(&["gate"], "deny Net\npure core\n");
assert_eq!(rc, Some(0), "…and still passes when nothing violates:\n{err}");
std::fs::write(&pol, "deny Nett app\nallow Nett host.example\n").unwrap();
let out = Command::new(bin())
.arg("parsepolicy")
.arg(&pol)
.env_remove("CANDOR_CONFIG")
.output()
.expect("run parsepolicy");
assert_eq!(out.status.code(), Some(0), "parsepolicy MUST NOT refuse a policy it can read");
let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
let rules: Vec<&str> =
v["errors"].as_array().unwrap().iter().map(|e| e["rule"].as_str().unwrap()).collect();
assert_eq!(rules, vec!["deny Nett app", "allow Nett host.example"], "…it reports both: {v}");
}
#[test]
fn whatif_over_an_incomplete_report_omits_ok_rather_than_answering_either_boolean() {
let f = Fixture::new("whatif-incomplete");
let report = |unanalyzed: &str| {
format!(
r#"{{"candor":{{"version":"handwritten","spec":"0.24"}},"package":"app",
"analyzed":{{"count":2,"digest":"0"}},{unanalyzed}
"functions":[{{"fn":"app.handler","inferred":[],"direct":[]}},
{{"fn":"app.leaf","inferred":[],"direct":[]}}]}}"#
)
};
let whatif = |sub: &str, unanalyzed: &str, policy: &str| -> (i32, serde_json::Value, String) {
let loc = gate_fixture(&f.dir, sub, &report(unanalyzed), Some(r#"{"app.handler":["app.leaf"]}"#));
let p = pol(&f.dir, sub, policy);
let out = Command::new(bin())
.args(["whatif", "app.leaf", "Net", "--report", &loc, "--policy", &p.to_string_lossy(), "--json"])
.output()
.expect("run candor-query");
(
out.status.code().unwrap_or(-1),
serde_json::from_slice(&out.stdout).expect("a JSON document"),
String::from_utf8_lossy(&out.stdout).into_owned(),
)
};
const MANIFEST: &str = r#""unanalyzed":[{"path":"src/opaque.rs","reason":"parse error"}],"#;
let (rc, v, _) = whatif("clean", MANIFEST, "deny Net app.elsewhere\n");
assert!(
v.get("ok").is_none(),
"`ok` must be ABSENT — a consumer writing `if (r.ok)` has to get a falsy value and fail safe, \
and `ok: false` would assert a violation the analysis never found:\n{v:#}"
);
assert_eq!(v["incomplete"], serde_json::json!(true), "{v:#}");
assert_eq!(
v["unanalyzed"],
serde_json::json!([{"path": "src/opaque.rs", "reason": "parse error"}]),
"the manifest travels, so a consumer that looks past `incomplete` learns exactly what was \
unread:\n{v:#}"
);
assert_eq!(
v["affected"],
serde_json::json!(["app.handler", "app.leaf"]),
"the blast radius is hedged, never withheld:\n{v:#}"
);
assert_eq!(v["violations"], serde_json::json!([]), "{v:#}");
assert_eq!(rc, 0, "the exit code is unchanged — §3.3's exit-2 causes are a GATE's, and this is not one");
let (rc2, v2, _) = whatif("firing", MANIFEST, "deny Net app\n");
assert!(v2.get("ok").is_none(), "{v2:#}");
assert_eq!(v2["incomplete"], serde_json::json!(true), "{v2:#}");
assert_eq!(v2["violations"].as_array().unwrap().len(), 2, "the finding is not suppressed:\n{v2:#}");
assert_eq!(rc2, 1, "a certain violation still fails:\n{v2:#}");
let (rc3, v3, _) = whatif("complete", "", "deny Net app.elsewhere\n");
assert_eq!(
v3["ok"],
serde_json::json!(true),
"a COMPLETE report must still get its answer — dropping `ok` unconditionally would satisfy every \
absence assert above and delete the verb for everyone with no unanalyzed code:\n{v3:#}"
);
assert!(v3.get("incomplete").is_none(), "{v3:#}");
assert!(v3.get("unanalyzed").is_none(), "{v3:#}");
assert_eq!(rc3, 0);
let (_, v4, _) = whatif("emptymanifest", r#""unanalyzed":[],"#, "deny Net app.elsewhere\n");
assert_eq!(v4["ok"], serde_json::json!(true), "{v4:#}");
assert!(v4.get("incomplete").is_none(), "{v4:#}");
let human = |sub: &str, unanalyzed: &str| -> String {
let loc = gate_fixture(&f.dir, sub, &report(unanalyzed), Some(r#"{"app.handler":["app.leaf"]}"#));
let p = pol(&f.dir, sub, "deny Net app.elsewhere\n");
let out = Command::new(bin())
.args(["whatif", "app.leaf", "Net", "--report", &loc, "--policy", &p.to_string_lossy()])
.output()
.expect("run candor-query");
String::from_utf8_lossy(&out.stdout).into_owned()
};
let h = human("human-incomplete", MANIFEST);
assert!(h.contains("INCOMPLETE"), "the operator is told the universe was partial:\n{h}");
assert!(h.contains("src/opaque.rs"), "…and what was unread:\n{h}");
assert!(
!h.contains("✓ within policy"),
"the `✓` is withheld for the reason `ok` is — it is a claim over a set known to be partial:\n{h}"
);
let h2 = human("human-complete", "");
assert!(h2.contains("✓ within policy"), "{h2}");
assert!(!h2.contains("INCOMPLETE"), "no hedge where none is owed:\n{h2}");
}
#[test]
fn whatif_treats_an_unreadable_unanalyzed_key_as_incomplete_rather_than_as_empty() {
let f = Fixture::new("whatif-corrupt-manifest");
let loc = gate_fixture(
&f.dir,
"r",
r#"{"candor":{"version":"handwritten","spec":"0.24"},"package":"app",
"analyzed":{"count":1,"digest":"0"},
"unanalyzed":[{"unit":"src/opaque.rs","why":"parse error"}],
"functions":[{"fn":"app.leaf","inferred":[],"direct":[]}]}"#,
Some(r#"{"app.leaf":[]}"#),
);
let p = pol(&f.dir, "w", "deny Net app.elsewhere\n");
let out = Command::new(bin())
.args(["whatif", "app.leaf", "Net", "--report", &loc, "--policy", &p.to_string_lossy(), "--json"])
.output()
.expect("run candor-query");
let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("a JSON document");
let err = String::from_utf8_lossy(&out.stderr);
assert!(v.get("ok").is_none(), "an unreadable manifest is not an empty one:\n{v:#}\n{err}");
assert_eq!(v["incomplete"], serde_json::json!(true), "{v:#}");
assert!(v.get("unanalyzed").is_none(), "a manifest that could not be read is not fabricated:\n{v:#}");
assert!(err.contains("`unanalyzed` key is PRESENT"), "the key is named on stderr: {err}");
}
#[test]
fn an_advisory_verb_names_what_the_gate_could_not_judge_and_stays_quiet_where_it_could() {
let f = Fixture::new("advisory-bound");
let refused_loc = gate_fixture(
&f.dir,
"refused",
r#"{"candor":{"version":"handwritten","spec":"0.23"},"package":"app",
"analyzed":{"count":3,"digest":"0"},
"functions":[
{"fn":"app.nativeHole","inferred":["Unknown"],"direct":["Unknown"],"unknownWhy":["native:dlopen"]},
{"fn":"app.noClass","inferred":["Net"],"direct":["Net"],"hosts":["api.example.com"]},
{"fn":"app.noClass2","inferred":["Net"],"direct":["Net"],"hosts":["b.example.com"]},
{"fn":"app.writes","inferred":["Fs"],"direct":["Fs"],"paths":["/etc/hosts"]}]}"#,
None,
);
let answered_loc = gate_fixture(
&f.dir,
"answered",
r#"{"candor":{"version":"handwritten","spec":"0.23"},"package":"app",
"analyzed":{"count":3,"digest":"0"},
"functions":[
{"fn":"app.nativeHole","inferred":["Unknown"],"direct":["Unknown"],"unknownWhy":["native:dlopen"]},
{"fn":"app.hasClass","inferred":["Net"],"direct":["Net"],"hosts":["api.example.com"],
"netClass":["unknown-host"]},
{"fn":"app.writes","inferred":["Fs"],"direct":["Fs"],"paths":["/etc/hosts"]}]}"#,
None,
);
let netclass = pol(&f.dir, "netclass", "deny Net[unknown-host] app\n");
let run = |verb: &str, loc: &str, extra: &[&str]| -> (i32, String, String) {
let mut args: Vec<String> =
vec![verb.into(), "--report".into(), loc.into(), "--policy".into(),
netclass.to_string_lossy().into_owned()];
args.extend(extra.iter().map(|s| s.to_string()));
let out = Command::new(bin()).args(&args).env_remove("CANDOR_POLICY").output().expect("run");
(
out.status.code().unwrap_or(-1),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
};
let json = |s: &str| -> serde_json::Value {
serde_json::from_str(s).unwrap_or_else(|e| panic!("not JSON ({e}):\n{s}"))
};
let (grc, _, gerr) = run_gate(&refused_loc, &netclass, &[]);
assert_eq!(grc, 2, "the gate CANNOT judge `app.noClass` over this report:\n{gerr}");
assert!(gerr.contains("app.noClass"), "…and it says which function:\n{gerr}");
let (urc, uout, _) = run("unverified", &refused_loc, &["--json"]);
let u = json(&uout);
let named: Vec<&str> =
u["unverified"].as_array().unwrap().iter().map(|h| h["fn"].as_str().unwrap()).collect();
assert!(
named.contains(&"app.noClass"),
"the gate exited 2 — it could NOT clear `app.noClass` — yet the verb does not name it \
(named: {named:?}). The advisory verb is more confident than the gate over identical bytes:\n{uout}"
);
assert!(
named.contains(&"app.noClass2"),
"the SECOND unjudgeable function too — the gate reports one example per rule, this verb reports \
the functions (named: {named:?}):\n{uout}"
);
assert!(named.contains(&"app.nativeHole"), "the ordinary hole is still named:\n{uout}");
let refusal = u["unverified"]
.as_array()
.unwrap()
.iter()
.find(|h| h["fn"] == "app.noClass")
.expect("named above");
let why = refusal["why"].as_str().unwrap_or("");
assert!(
why.contains("netClass") && why.contains("absent"),
"the reason recorded is the MISSING EVIDENCE:\n{why}"
);
assert!(
!why.contains("unknown-host") || why.contains("no `netClass`"),
"…and NEVER the derived class — this engine can floor `app.noClass` at `unknown-host` from its \
`hosts` in one line, and recording that would restate the defect as a disclosure:\n{why}"
);
assert!(
refusal["upgrade"].is_null(),
"no policy edit makes a missing field appear, so there is no upgrade to advise:\n{uout}"
);
let verdict = f.dir.join("v.json");
let _ = std::fs::remove_file(&verdict);
let _ = run_gate(&refused_loc, &netclass, &["--gate-json", &verdict.to_string_lossy()]);
let gdoc: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&verdict).expect("gate document")).unwrap();
assert_eq!(
u["unevaluated"], gdoc["unevaluated"],
"the disclosure is the gate's, byte for byte — inventing a second spelling is the mistake SPEC \
§3.2 says this document has made four times:\n{uout}"
);
assert_eq!(u["unevaluated"].as_array().map(Vec::len), Some(1), "one entry per RULE:\n{uout}");
assert!(
u.get("ok").is_none(),
"the KEY IS GONE — `assert_eq!(ok, false)` would pass on the fabrication mirror:\n{uout}"
);
assert_eq!(urc, 0, "advisory by default — the ruling is about the DISCLOSURE");
let (src, _, _) = run("unverified", &refused_loc, &["--json", "--strict"]);
assert_eq!(src, 2, "`--strict` exits 2, MATCHING the gate — answering 1 would claim it got further");
let (_, utext, _) = run("unverified", &refused_loc, &[]);
assert!(utext.contains("app.noClass"), "the operator is told too:\n{utext}");
assert!(
utext.contains("COULD NOT JUDGE"),
"…and told it is a refusal, not an ordinary hole:\n{utext}"
);
let (frc, fout, _) = run("fix-gate", &refused_loc, &["--json"]);
let fg = json(&fout);
assert_eq!(fg["remedies"].as_array().map(Vec::len), Some(0), "no hoist plan for it:\n{fout}");
assert!(
fg.get("ok").is_none(),
"`ok: true` asserts there is no crossing over a boundary nothing adjudicated, and `ok: false` \
would assert a crossing never found (the fabrication mirror, SPEC §3.2 `0075987`) — so the KEY \
IS GONE. `assert_eq!(ok, false)` would have passed on the wrong fix:\n{fout}"
);
assert_eq!(fg["unevaluated"], gdoc["unevaluated"], "same shape, same list:\n{fout}");
assert_eq!(frc, 0, "advisory by default");
let (fsrc, _, _) = run("fix-gate", &refused_loc, &["--json", "--strict"]);
assert_eq!(fsrc, 2, "`--strict` exits 2, matching the gate");
let (_, ftext, ferr) = run("fix-gate", &refused_loc, &[]);
assert!(!ftext.contains('✓'), "the tick is the same claim in prose:\n{ftext}");
assert!(ferr.contains("app.noClass"), "the operator learns which boundary went unjudged:\n{ferr}");
let (xrc, xout, xerr) = {
let out = Command::new(bin())
.args(["fix", "app.noClass", "Net", "--report", &refused_loc, "--policy"])
.arg(&netclass)
.arg("--json")
.env_remove("CANDOR_POLICY")
.output()
.expect("run");
(
out.status.code().unwrap_or(-1),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
};
let x = json(&xout);
assert!(x.get("hoistTo").is_none() && x.get("deniedSpan").is_none(),
"a hoist plan for a boundary the gate could not adjudicate is a confident instruction resting \
on a guess:\n{xout}");
assert_eq!(x["unevaluated"], gdoc["unevaluated"], "it says WHY it computed nothing:\n{xout}");
assert_eq!(xrc, 0);
assert!(xerr.contains("netClass"), "and on the human channel:\n{xerr}");
let (grc2, _, gerr2) = run_gate(&answered_loc, &netclass, &[]);
assert_eq!(grc2, 1, "`app.hasClass` carries the class the filter names, so the gate CHARGES:\n{gerr2}");
let (urc2, uout2, _) = run("unverified", &answered_loc, &["--json"]);
let u2 = json(&uout2);
let named2: Vec<&str> =
u2["unverified"].as_array().unwrap().iter().map(|h| h["fn"].as_str().unwrap()).collect();
assert_eq!(
named2,
vec!["app.nativeHole"],
"a function the gate CAN judge must NOT start being named — the over-report mirror, and the \
direction a fabrication fix introduces:\n{uout2}"
);
assert!(
u2.get("unevaluated").is_none(),
"an ordinary document stays byte-identical to a pre-ruling one:\n{uout2}"
);
assert_eq!(
u2["ok"],
serde_json::json!(false),
"a report the gate CAN judge still gets a boolean, and here it is `false` because a hole WAS \
found — that is a finding, not a declined question:\n{uout2}"
);
assert_eq!(urc2, 0);
let (src2, _, _) = run("unverified", &answered_loc, &["--json", "--strict"]);
assert_eq!(src2, 1, "…and `--strict` is back to 1: holes, but nothing unevaluated");
let (_, utext2, _) = run("unverified", &answered_loc, &[]);
assert!(
utext2.contains("The gate still PASSES — "),
"the unqualified sentence returns where nothing went unevaluated — 224 OLD/NEW runs over four \
corpora showed this line was the whole of the churn until it was made conditional:\n{utext2}"
);
let (frc2, fout2, _) = run("fix-gate", &answered_loc, &["--json"]);
let fg2 = json(&fout2);
assert_eq!(fg2["ok"], false, "`ok` COMES BACK where every rule was answerable:\n{fout2}");
assert!(fg2.get("unevaluated").is_none(), "…and nothing went unevaluated:\n{fout2}");
assert_eq!(
fg2["remedies"].as_array().map(Vec::len),
Some(1),
"…and the crossing the gate DOES charge still gets its remedy — a fix that turned `fix-gate` \
silent on real violations would pass every absence assert above:\n{fout2}"
);
assert_eq!(frc2, 0);
let out = Command::new(bin())
.args(["fix", "app.hasClass", "Net", "--report", &answered_loc, "--policy"])
.arg(&netclass)
.arg("--json")
.env_remove("CANDOR_POLICY")
.output()
.expect("run");
let x2 = json(&String::from_utf8_lossy(&out.stdout));
assert_eq!(x2["fn"], "app.hasClass");
assert!(x2.get("deniedSpan").is_some(), "`fix` still plans where the gate charges:\n{x2}");
}
#[test]
fn an_advisory_verb_over_an_incomplete_report_omits_ok_on_every_channel_it_answers_on() {
let f = Fixture::new("advisory-incomplete");
let report = |unanalyzed: &str| {
format!(
r#"{{"candor":{{"version":"handwritten","spec":"0.24"}},"package":"app",
"analyzed":{{"count":3,"digest":"0"}},{unanalyzed}
"functions":[{{"fn":"app.reader","inferred":["Fs"],"direct":["Fs"],"paths":["/x"]}},
{{"fn":"app.hole","inferred":["Unknown"],"unknownWhy":["a fn-pointer call"]}},
{{"fn":"app.plain","inferred":[]}}]}}"#
)
};
const MANIFEST: &str = r#""unanalyzed":[{"path":"src/opaque.rs","reason":"parse error"}],"#;
let run = |sub: &str, unanalyzed: &str, verb: &str, policy: &str, extra: &[&str]| -> (i32, String, String) {
let loc = gate_fixture(&f.dir, sub, &report(unanalyzed), Some(r#"{"app.reader":[],"app.hole":[],"app.plain":[]}"#));
let p = pol(&f.dir, sub, policy);
let mut args: Vec<String> =
vec![verb.into(), "--report".into(), loc, "--policy".into(), p.to_string_lossy().into_owned()];
args.extend(extra.iter().map(|s| s.to_string()));
let out = Command::new(bin()).args(&args).output().expect("run candor-query");
(
out.status.code().unwrap_or(-1),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
};
let jrun = |sub: &str, unanalyzed: &str, verb: &str, policy: &str, extra: &[&str]| -> (i32, serde_json::Value) {
let (rc, so, se) = run(sub, unanalyzed, verb, policy, extra);
(rc, serde_json::from_str(&so).unwrap_or_else(|e| panic!("a JSON document ({e}):\n{so}\n{se}")))
};
let manifest = serde_json::json!([{"path": "src/opaque.rs", "reason": "parse error"}]);
for verb in ["unverified", "fix-gate"] {
let findings = if verb == "unverified" { "unverified" } else { "remedies" };
let (rc, v) = jrun(&format!("{verb}-incomplete"), MANIFEST, verb, "deny Net app.elsewhere\n", &["--json", "--strict"]);
assert!(
v.get("ok").is_none(),
"{verb}: `ok` must be ABSENT — a consumer writing `if (r.ok)` has to get a falsy value and \
fail safe, and `ok: false` would assert a finding the analysis never made:\n{v:#}"
);
assert_eq!(v["incomplete"], serde_json::json!(true), "{verb}:\n{v:#}");
assert_eq!(v["unanalyzed"], manifest, "{verb}: the manifest travels:\n{v:#}");
assert_eq!(v[findings], serde_json::json!([]), "{verb}: the findings still ship:\n{v:#}");
assert_eq!(rc, 2, "{verb} --strict exits 2 — `gate --report` does over these bytes:\n{v:#}");
let (rc_adv, v_adv) = jrun(&format!("{verb}-advisory"), MANIFEST, verb, "deny Net app.elsewhere\n", &["--json"]);
assert_eq!(rc_adv, 0, "{verb}:\n{v_adv:#}");
assert_eq!(v_adv["incomplete"], serde_json::json!(true), "{verb}:\n{v_adv:#}");
let (rc2, v2) = jrun(&format!("{verb}-complete"), "", verb, "deny Net app.elsewhere\n", &["--json", "--strict"]);
assert_eq!(
v2["ok"],
serde_json::json!(true),
"{verb}: a COMPLETE report must still get its answer — omitting `ok` unconditionally would \
satisfy every absence assert above and delete the field for everyone:\n{v2:#}"
);
assert!(v2.get("incomplete").is_none(), "{verb}:\n{v2:#}");
assert!(v2.get("unanalyzed").is_none(), "{verb}:\n{v2:#}");
assert_eq!(rc2, 0, "{verb}:\n{v2:#}");
let (_, v3) = jrun(&format!("{verb}-emptymanifest"), r#""unanalyzed":[],"#, verb, "deny Net app.elsewhere\n", &["--json"]);
assert_eq!(v3["ok"], serde_json::json!(true), "{verb}:\n{v3:#}");
assert!(v3.get("incomplete").is_none(), "{verb}:\n{v3:#}");
}
let (rc, v) = jrun("fixgate-firing", MANIFEST, "fix-gate", "deny Fs app\n", &["--json", "--strict"]);
assert!(v.get("ok").is_none(), "{v:#}");
assert_eq!(v["incomplete"], serde_json::json!(true), "{v:#}");
assert_eq!(v["remedies"].as_array().unwrap().len(), 1, "the remedy is hedged, never withheld:\n{v:#}");
assert_eq!(rc, 2, "{v:#}");
let (rc2, v2) = jrun("fixgate-firing-complete", "", "fix-gate", "deny Fs app\n", &["--json", "--strict"]);
assert_eq!(v2["ok"], serde_json::json!(false), "{v2:#}");
assert_eq!(rc2, 1, "{v2:#}");
let (rc_h0, h0, _) =
run("human-incomplete-clean", MANIFEST, "unverified", "deny Net app.elsewhere\n", &["--strict"]);
assert!(
!h0.contains("PROVABLY clean (no Unknown holes) ✓"),
"\"PROVABLY clean\" over a report declaring source candor could not read — the prose `ok: true`, \
on the one branch where it is the entire answer:\n{h0}"
);
assert!(h0.contains("INCOMPLETE") && h0.contains("src/opaque.rs"), "{h0}");
assert_eq!(rc_h0, 2, "…and the human channel's exit code is the JSON channel's:\n{h0}");
let (_, h, _) = run("human-incomplete", MANIFEST, "unverified", "deny Net app\n", &[]);
assert!(h.contains("INCOMPLETE"), "the operator is told the universe was partial:\n{h}");
assert!(h.contains("src/opaque.rs"), "…and exactly what was unread:\n{h}");
assert!(
!h.contains("PROVABLY clean (no Unknown holes) ✓"),
"the `✓` is the prose `ok: true` and is withheld for the same reason:\n{h}"
);
assert!(
!h.contains("The gate still PASSES"),
"and so is this one, which is not merely unhedged but FALSE — `gate --report` exits 2 over \
these bytes, so the gate does not pass:\n{h}"
);
assert!(h.contains("app.hole"), "the finding still ships on this channel too:\n{h}");
let (_, hf, _) = run("human-incomplete-fg", MANIFEST, "fix-gate", "deny Net app.elsewhere\n", &[]);
assert!(hf.contains("INCOMPLETE") && !hf.contains("no deny/pure boundary crossings in this report ✓"), "{hf}");
let (_, h2, _) = run("human-complete", "", "unverified", "deny Net app\n", &[]);
assert!(h2.contains("The gate still PASSES"), "no hedge where none is owed:\n{h2}");
assert!(!h2.contains("INCOMPLETE"), "{h2}");
let (_, h3, _) = run("human-complete-clean", "", "unverified", "deny Net app.elsewhere\n", &[]);
assert!(h3.contains("PROVABLY clean (no Unknown holes) ✓"), "{h3}");
let (_, h4, _) = run("human-complete-fg", "", "fix-gate", "deny Net app.elsewhere\n", &[]);
assert!(h4.contains("no deny/pure boundary crossings in this report ✓"), "{h4}");
}
#[test]
fn an_advisory_verb_is_never_less_sensitive_to_incompleteness_than_the_gate() {
let f = Fixture::new("advisory-gate-bound");
let body = |unanalyzed: &str| {
format!(
r#"{{"candor":{{"version":"handwritten","spec":"0.24"}},"package":"app",
"analyzed":{{"count":2,"digest":"0"}},{unanalyzed}
"functions":[{{"fn":"app.reader","inferred":["Fs"],"direct":["Fs"],"paths":["/x"]}},
{{"fn":"app.plain","inferred":[]}}]}}"#
)
};
for (sub, unanalyzed) in [
("readable", r#""unanalyzed":[{"path":"src/opaque.rs","reason":"parse error"}],"#),
("wrongfields", r#""unanalyzed":[{"unit":"src/opaque.rs","why":"parse error"}],"#),
("barestrings", r#""unanalyzed":["src/opaque.rs"],"#),
("mixed", r#""unanalyzed":[{"path":"src/a.rs","reason":"parse error"},{"unit":"src/b.rs"}],"#),
] {
let loc = gate_fixture(&f.dir, sub, &body(unanalyzed), Some(r#"{"app.reader":[],"app.plain":[]}"#));
let p = pol(&f.dir, sub, "deny Net app.elsewhere\n");
let go = |verb: &str| -> (i32, serde_json::Value) {
let out = Command::new(bin())
.args([verb, "--report", &loc, "--policy", &p.to_string_lossy(), "--json", "--strict"])
.output()
.expect("run candor-query");
let so = String::from_utf8_lossy(&out.stdout).into_owned();
(out.status.code().unwrap_or(-1), serde_json::from_str(&so).unwrap_or(serde_json::Value::Null))
};
let (grc, gv) = go("gate");
assert_ne!(grc, 0, "[{sub}] the control: the gate must not clear this report:\n{gv:#}");
for verb in ["unverified", "fix-gate"] {
let (rc, v) = go(verb);
assert!(
v.get("ok").is_none(),
"[{sub}] {verb} read a manifest the gate could not — an element that cannot be read is \
still an element saying something was not analysed:\n{v:#}"
);
assert_eq!(v["incomplete"], serde_json::json!(true), "[{sub}] {verb}:\n{v:#}");
assert_eq!(rc, 2, "[{sub}] {verb} must not exit smaller than the gate:\n{v:#}");
}
}
let loc = gate_fixture(&f.dir, "wrongfields", &body(r#""unanalyzed":[{"unit":"x","why":"y"}],"#), None);
let p = pol(&f.dir, "wrongfields", "deny Net app.elsewhere\n");
let out = Command::new(bin())
.args(["unverified", "--report", &loc, "--policy", &p.to_string_lossy(), "--json"])
.output()
.expect("run");
let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("a JSON document");
assert!(v.get("unanalyzed").is_none(), "a manifest that could not be read is not invented:\n{v:#}");
assert!(
String::from_utf8_lossy(&out.stderr).contains("`unanalyzed` key is PRESENT"),
"the key is NAMED: {}",
String::from_utf8_lossy(&out.stderr)
);
let loc = gate_fixture(
&f.dir,
"fixverb",
&body(r#""unanalyzed":[{"path":"src/opaque.rs","reason":"parse error"}],"#),
Some(r#"{"app.reader":[],"app.plain":[]}"#),
);
let p = pol(&f.dir, "fixverb", "deny Fs app\n");
let fix = |args: &[&str]| -> (i32, String, String) {
let mut a: Vec<String> = vec!["fix".into(), "app.reader".into(), "Fs".into(), "--report".into(), loc.clone(),
"--policy".into(), p.to_string_lossy().into_owned()];
a.extend(args.iter().map(|s| s.to_string()));
let out = Command::new(bin()).args(&a).output().expect("run");
(
out.status.code().unwrap_or(-1),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
};
let (rc, so, se) = fix(&["--json"]);
let v: serde_json::Value = serde_json::from_str(&so).unwrap_or_else(|e| panic!("a plan document ({e}):\n{so}"));
assert_eq!(v["incomplete"], serde_json::json!(true), "{v:#}");
assert!(v.get("deniedSpan").is_some(), "the plan is hedged, never withheld:\n{v:#}");
assert!(
se.contains("INCOMPLETE"),
"…and the note is on STDERR here, because stdout is a document and prose beside it would \
corrupt it: {se}"
);
assert_eq!(rc, 0, "the exit code is unchanged: this verb answers no `ok` for `--strict` to follow");
let (_, so2, _) = fix(&[]);
assert!(so2.contains("INCOMPLETE") && so2.contains("src/opaque.rs"), "{so2}");
let loc2 = gate_fixture(&f.dir, "fixverb-complete", &body(""), Some(r#"{"app.reader":[],"app.plain":[]}"#));
let out = Command::new(bin())
.args(["fix", "app.reader", "Fs", "--report", &loc2, "--policy", &p.to_string_lossy(), "--json"])
.output()
.expect("run");
let v2: serde_json::Value = serde_json::from_slice(&out.stdout).expect("a plan document");
assert!(v2.get("incomplete").is_none() && v2.get("unanalyzed").is_none(), "{v2:#}");
assert!(v2.get("deniedSpan").is_some(), "{v2:#}");
}
#[test]
fn the_advisory_verbs_read_the_same_report_set_the_gate_does() {
let f = Fixture::new("advisory-report-set");
let d = f.dir.join("twin");
std::fs::create_dir_all(&d).unwrap();
std::fs::write(
d.join("report.alpha.scan.json"),
r#"{"candor":{"version":"handwritten","spec":"0.24"},"package":"alpha",
"analyzed":{"count":1,"digest":"0"},
"functions":[{"fn":"alpha.plain","inferred":[]}]}"#,
)
.unwrap();
std::fs::write(
d.join("report.beta.scan.json"),
r#"{"candor":{"version":"handwritten","spec":"0.24"},"package":"beta",
"analyzed":{"count":1,"digest":"0"},
"unanalyzed":[{"path":"src/opaque.rs","reason":"parse error"}],
"functions":[{"fn":"beta.plain","inferred":[]}]}"#,
)
.unwrap();
let loc = d.join("report").to_string_lossy().into_owned();
let p = pol(&f.dir, "set", "deny Net app\n");
let go = |verb: &str, extra: &[&str]| -> (i32, serde_json::Value) {
let mut a: Vec<String> =
vec![verb.into(), "--report".into(), loc.clone(), "--policy".into(),
p.to_string_lossy().into_owned(), "--json".into()];
a.extend(extra.iter().map(|s| s.to_string()));
let out = Command::new(bin()).args(&a).output().expect("run candor-query");
let so = String::from_utf8_lossy(&out.stdout).into_owned();
(
out.status.code().unwrap_or(-1),
serde_json::from_str(&so).unwrap_or_else(|e| panic!("[{verb}] a JSON document ({e}):\n{so}")),
)
};
let (grc, gv) = go("gate", &[]);
assert_eq!(grc, 2, "the control: the gate reads the SET and finds the sibling's manifest:\n{gv:#}");
assert_eq!(gv["unanalyzed"].as_array().map(Vec::len), Some(1), "…and carries it:\n{gv:#}");
for verb in ["unverified", "fix-gate"] {
let (rc, v) = go(verb, &["--strict"]);
assert_eq!(
v["unanalyzed"], gv["unanalyzed"],
"[{verb}] the same list off the same set — a shorter one is not less certain than the gate, \
it is a different question:\n{v:#}"
);
assert!(v.get("ok").is_none(), "[{verb}]:\n{v:#}");
assert_eq!(rc, 2, "[{verb}]:\n{v:#}");
}
let one = d.join("report.alpha.scan.json").to_string_lossy().into_owned();
let out = Command::new(bin())
.args(["unverified", "--report", &one, "--policy", &p.to_string_lossy(), "--json", "--strict"])
.output()
.expect("run");
let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("a JSON document");
assert_eq!(v["ok"], serde_json::json!(true), "the sibling's manifest is not pulled in:\n{v:#}");
assert_eq!(out.status.code().unwrap_or(-1), 0, "{v:#}");
}
#[test]
fn gains_names_which_report_judged_nothing_on_either_side() {
let f = Fixture::new("gainsjudged");
let base_pre = format!("{}.base", f.prefix);
let cur_pre = format!("{}.cur", f.prefix);
let judged_nothing = r#"{"candor":{"version":"t","spec":"0.28"},"package":"lib","functions":[],"analyzed":{"count":0,"digest":"0"}}"#;
let intact = r#"{"candor":{"version":"t","spec":"0.28"},"package":"lib","functions":[{"fn":"lib::f","loc":"s:1","inferred":["Fs"],"hash":"h"}],"analyzed":{"count":1,"digest":"0"}}"#;
let base_file = format!("{base_pre}.lib.scan.json");
let cur_file = format!("{cur_pre}.lib.scan.json");
std::fs::write(&base_file, judged_nothing).unwrap();
std::fs::write(&cur_file, intact).unwrap();
let out = Command::new(bin()).args(["gains", &cur_pre, &base_pre, "--json"]).output().expect("run");
assert_eq!(out.status.code(), Some(0), "gains stays advisory — the manifest is a disclosure, not an exit code");
let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("json");
assert_eq!(v["baselineIncomplete"], serde_json::json!(true), "the flag still rides: {v}");
assert_eq!(v["baselineJudgedNothing"], serde_json::json!([base_file.clone()]),
"the ARRAY-OF-PATHS shape, naming WHICH baseline report judged nothing: {v}");
assert!(v.get("incomplete").is_none() && v.get("judgedNothing").is_none(),
"the intact CURRENT side carries no hedge — the two sides fail differently and are disclosed separately: {v}");
std::fs::write(&base_file, intact).unwrap();
std::fs::write(&cur_file, judged_nothing).unwrap();
let out = Command::new(bin()).args(["gains", &cur_pre, &base_pre, "--json"]).output().expect("run");
let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("json");
assert_eq!(v["incomplete"], serde_json::json!(true), "{v}");
assert_eq!(v["judgedNothing"], serde_json::json!([cur_file]),
"the same treatment on the current side — `incomplete: true` alone cannot name the report: {v}");
assert!(v.get("baselineJudgedNothing").is_none(), "the intact BASELINE side stays clean: {v}");
std::fs::write(&cur_file, intact).unwrap();
let out = Command::new(bin()).args(["gains", &cur_pre, &base_pre, "--json"]).output().expect("run");
let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("json");
for k in ["incomplete", "judgedNothing", "baselineIncomplete", "baselineJudgedNothing"] {
assert!(v.get(k).is_none(), "an intact pair must not carry `{k}`: {v}");
}
}
#[test]
fn gate_json_naming_an_expanded_report_is_refused_with_the_report_intact() {
let f = Fixture::new("gatelocator");
f.write_report();
let policy = f.dir.join("policy");
std::fs::write(&policy, "deny Fs\n").unwrap();
let report = f.report_path();
let before = std::fs::read(&report).unwrap();
let out = Command::new(bin())
.args(["gate", "--report", &f.prefix, "--policy", policy.to_string_lossy().as_ref(),
"--gate-json", &report])
.env_remove("CANDOR_POLICY").env_remove("CANDOR_CONFIG").env_remove("CANDOR_REPORT")
.output().expect("run candor-query");
let stderr = String::from_utf8(out.stderr).unwrap();
assert_eq!(out.status.code(), Some(2),
"a sink naming one of the locator's expanded reports is refused: {stderr}");
assert!(stderr.contains("names a file this gate reads"),
"the refusal names the collision, not a downstream parse failure: {stderr}");
assert_eq!(std::fs::read(&report).unwrap(), before,
"the report's BYTES are untouched — before the fix this file held the armed refusal document");
}
#[test]
fn gate_json_naming_a_discovered_report_is_refused_with_the_report_intact() {
let f = Fixture::new("gatediscovery");
let candor = f.dir.join(".candor");
std::fs::create_dir_all(&candor).unwrap();
let report = candor.join("report.rpt.scan.json");
std::fs::write(&report,
r#"{"candor":{"version":"t","spec":"0.28"},"package":"rpt","functions":[{"fn":"inner","loc":"s:1","inferred":["Fs"],"hash":"h","paths":["/x"]}]}"#).unwrap();
let policy = f.dir.join("policy");
std::fs::write(&policy, "deny Fs\n").unwrap();
let before = std::fs::read(&report).unwrap();
let out = Command::new(bin())
.args(["gate", "--policy", "policy", "--gate-json", ".candor/report.rpt.scan.json"])
.current_dir(&f.dir)
.env_remove("CANDOR_POLICY").env_remove("CANDOR_CONFIG").env_remove("CANDOR_REPORT")
.output().expect("run candor-query");
let stderr = String::from_utf8(out.stderr).unwrap();
assert_eq!(out.status.code(), Some(2), "the discovered report is an input: {stderr}");
assert_eq!(std::fs::read(&report).unwrap(), before,
"the discovered report's bytes are untouched — this spelling destroyed it before the fix");
}
#[test]
fn gate_json_naming_the_reports_sidecar_is_refused_and_a_gate_json_sibling_still_gates() {
let f = Fixture::new("gatesidecar");
f.write_report();
let policy = f.dir.join("policy");
std::fs::write(&policy, "deny Fs\n").unwrap();
let sidecar = format!("{}.rpt.scan.callgraph.json", f.prefix);
let side_before = std::fs::read(&sidecar).unwrap();
let out = Command::new(bin())
.args(["gate", "--report", &f.prefix, "--policy", policy.to_string_lossy().as_ref(),
"--gate-json", &sidecar])
.env_remove("CANDOR_POLICY").env_remove("CANDOR_CONFIG").env_remove("CANDOR_REPORT")
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(2),
"the pair's other half is part of what the locator names — before the fix this exited 1, a \
SUCCESS, with a real verdict where the callgraph belonged");
assert_eq!(std::fs::read(&sidecar).unwrap(), side_before, "the sidecar's bytes are untouched");
let sink = format!("{}.rpt.scan.gate.json", f.prefix);
let report_before = std::fs::read(f.report_path()).unwrap();
let out = Command::new(bin())
.args(["gate", "--report", &f.prefix, "--policy", policy.to_string_lossy().as_ref(),
"--gate-json", &sink])
.env_remove("CANDOR_POLICY").env_remove("CANDOR_CONFIG").env_remove("CANDOR_REPORT")
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(1),
"deny Fs over the Fs fixture is a VIOLATION verdict, never a refusal — a guard that reddens \
the beside-the-report layout has not implemented the rule, it has broken the default");
let v: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&sink).unwrap()).expect("a real verdict document");
assert!(v.get("refused").is_none() && v["violations"].as_array().is_some_and(|a| !a.is_empty()),
"the sink carries the verdict, not the armed placeholder: {v}");
assert_eq!(std::fs::read(f.report_path()).unwrap(), report_before,
"…and the reports the gate read are byte-identical after the control run");
}
#[test]
fn the_verbs_that_answer_ok_still_refuse_over_an_unread_class() {
let f = Fixture::new("unread-refusal-control");
let report = r#"{"candor":{"version":"t","toolchain":"stable","spec":"0.32"},"package":"rpt",
"analyzed":{"count":2,"digest":"d"},
"excluded":[{"class":"non-library-target","count":1,"peeked":false,"reason":"tests/"}],
"functions":[
{"fn":"inner","loc":"s:1","inferred":["Fs"],"direct":["Fs"],"hash":"h1"},
{"fn":"outer","loc":"s:2","inferred":["Fs"],"hash":"h2","calls":["inner"]}]}"#;
std::fs::write(format!("{}.rpt.scan.json", f.prefix), report).unwrap();
let pol = write_policy(&f, "deny.policy", "deny Exec\n");
let out = Command::new(bin())
.args(["gate", "--report", &f.prefix, "--policy", &pol, "--json"])
.output()
.expect("run candor-query");
assert_eq!(out.status.code(), Some(2), "the gate REFUSES over a class nothing opened");
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert_eq!(v["ok"], serde_json::json!(false), "{v}");
assert_eq!(v["incomplete"], serde_json::json!(true), "{v}");
for argv in [vec!["unverified", "--strict"], vec!["fix-gate", "--strict"]] {
let mut args: Vec<&str> = argv.clone();
args.extend(["--report", &f.prefix, "--policy", &pol, "--json"]);
let out = Command::new(bin()).args(&args).output().expect("run candor-query");
assert_eq!(out.status.code(), Some(2),
"{argv:?}: an advisory verb must never be LESS sensitive than the gate over the same bytes");
let v: serde_json::Value =
serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert!(v.get("ok").is_none(), "{argv:?}: `ok` is OMITTED, never falsified: {v}");
assert_eq!(v["incomplete"], serde_json::json!(true), "{argv:?}: {v}");
}
for argv in [vec!["show", "inner"], vec!["map"]] {
let mut args: Vec<&str> = argv.clone();
args.extend(["--report", &f.prefix, "--json"]);
let out = Command::new(bin()).args(&args).output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0),
"{argv:?}: a descriptive verb's hedge is a DISCLOSURE, not an exit code");
let v: serde_json::Value =
serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert_eq!(v["incomplete"], serde_json::json!(true),
"{argv:?}: the hedge must still appear over an unread class: {v}");
}
}
#[test]
fn show_and_map_return_their_result_beside_the_caveat_when_hedging() {
let f = Fixture::new("runga-unanalyzed");
let report = r#"{"candor":{"version":"t","toolchain":"stable","spec":"0.28"},"package":"rpt",
"analyzed":{"count":2,"digest":"d"},
"unanalyzed":[{"path":"src/gen.rs","reason":"parse error"}],
"functions":[
{"fn":"inner","loc":"s:1","inferred":["Fs"],"direct":["Fs"],"hash":"h1"},
{"fn":"outer","loc":"s:2","inferred":["Fs"],"hash":"h2","calls":["inner"]}]}"#;
std::fs::write(format!("{}.rpt.scan.json", f.prefix), report).unwrap();
let j = Fixture::new("runga-judged");
let judged = r#"{"candor":{"version":"t","toolchain":"stable","spec":"0.28"},"package":"lib","functions":[],"analyzed":{"count":0,"digest":"0"}}"#;
let judged_file = format!("{}.lib.scan.json", j.prefix);
std::fs::write(&judged_file, judged).unwrap();
let out = Command::new(bin()).args(["show", "inner", "--report", &f.prefix, "--json"])
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0), "the hedge is a disclosure, not an exit code");
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert!(v.is_object(),
"show hedging must not answer the bare array — an array here is the pre-⟨0.28⟩ silent wrong \
answer, and the root type change is the loud stop: {v}");
assert_eq!(v["incomplete"], serde_json::json!(true));
assert_eq!(v["unanalyzed"][0]["path"], serde_json::json!("src/gen.rs"));
assert_eq!(v["functions"][0]["fn"], serde_json::json!("inner"),
"the result travels BESIDE the warning, never instead of it: {v}");
assert_eq!(v["functions"].as_array().map(Vec::len), Some(1), "{v}");
let out = Command::new(bin()).args(["map", "--report", &j.prefix, "--json"])
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert_eq!(v["incomplete"], serde_json::json!(true), "map hedging carries the caveat: {v}");
assert_eq!(v["judgedNothing"], serde_json::json!([judged_file]));
assert!(v["modules"].is_object(), "…and the (empty) map is present, not withheld: {v}");
let out = Command::new(bin()).args(["map", "--report", &f.prefix, "--json"])
.output().expect("run candor-query");
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert_eq!(v["incomplete"], serde_json::json!(true), "{v}");
let mods = v["modules"].as_object().expect("the module map rides beside the caveat");
assert!(!mods.is_empty(), "map hedging must still ANSWER — the rows are the thing: {v}");
assert!(v.as_object().unwrap().keys().all(|k| ["modules", "incomplete", "unanalyzed",
"judgedNothing", "noManifest", "unread"].contains(&k.as_str())),
"…and the ROOT carries only `modules` + the ⟨0.28⟩ caveat vocabulary — a module name at the root \
is the merged shape whose collision the nesting removes: {v}");
let h = Fixture::new("runga-healthy");
h.write_report();
let out = Command::new(bin()).args(["show", "inner", "--report", &h.prefix, "--json"])
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert!(v.is_array() && v[0]["fn"] == serde_json::json!("inner"),
"healthy show keeps its pinned top-level array: {v}");
let out = Command::new(bin()).args(["map", "--report", &h.prefix, "--json"])
.output().expect("run candor-query");
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert!(v.get("incomplete").is_none() && v.as_object().is_some_and(|o| !o.is_empty()),
"healthy map keeps its module rows and gains no caveat key: {v}");
}
#[test]
fn callers_impact_and_path_disclose_an_unread_class_beside_their_answer() {
let f = Fixture::new("cip-unread");
let report = r#"{"candor":{"version":"t","toolchain":"stable","spec":"0.32"},"package":"rpt",
"analyzed":{"count":3,"digest":"d"},
"excluded":[{"class":"non-library-target","count":1,"peeked":false,"reason":"tests/"}],
"functions":[
{"fn":"inner","loc":"s:1","inferred":["Fs"],"direct":["Fs"],"hash":"h1"},
{"fn":"wrapper","loc":"s:2","inferred":["Fs"],"hash":"h2","calls":["inner"]},
{"fn":"top","loc":"s:3","inferred":["Fs"],"hash":"h3","calls":["wrapper"]}]}"#;
std::fs::write(format!("{}.rpt.scan.json", f.prefix), report).unwrap();
std::fs::write(format!("{}.rpt.scan.callgraph.json", f.prefix),
r#"{"inner":[],"wrapper":["inner"],"top":["wrapper"]}"#).unwrap();
let out = Command::new(bin()).args(["callers", "wrapper", "--report", &f.prefix, "--json"])
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0),
"a descriptive verb's hedge is a DISCLOSURE, not an exit code (⟨0.24⟩)");
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert_eq!(v["incomplete"], serde_json::json!(true),
"callers answered flat over a class nothing opened: {v}");
assert_eq!(v["direct"], serde_json::json!(["top"]),
"…BESIDE the answer, never instead of it — the direct callers by NAME: {v}");
assert_eq!(v["transitive"], serde_json::json!(["top"]), "{v}");
assert_eq!(v["of"], serde_json::json!(["wrapper"]), "{v}");
let out = Command::new(bin())
.args(["callers", "wrapper", "--report", &f.prefix, "--json", "--include-unknown"])
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert_eq!(v["incomplete"], serde_json::json!(true),
"the frontier arm is the OTHER site of the same verb: {v}");
assert_eq!(v["direct"], serde_json::json!(["top"]), "{v}");
let out = Command::new(bin()).args(["impact", "wrapper", "--report", &f.prefix, "--json"])
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert_eq!(v["incomplete"], serde_json::json!(true), "impact answered flat: {v}");
assert_eq!(v["affected"], serde_json::json!(["top"]),
"…and the blast radius survives the hedge, by NAME: {v}");
assert_eq!(v["affectedCount"], serde_json::json!(1), "{v}");
assert_eq!(v["fn"], serde_json::json!("wrapper"), "{v}");
let out = Command::new(bin()).args(["path", "top", "Fs", "--report", &f.prefix, "--json"])
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert_eq!(v["incomplete"], serde_json::json!(true), "path answered flat: {v}");
let steps: Vec<&str> =
v["path"].as_array().expect("the chain rides beside the caveat")
.iter().map(|s| s["fn"].as_str().unwrap()).collect();
assert_eq!(steps, ["top", "wrapper", "inner"],
"…and the WHOLE chain is still there, in order: {v}");
let out = Command::new(bin()).args(["path", "top", "Net", "--report", &f.prefix, "--json"])
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert_eq!(v["incomplete"], serde_json::json!(true),
"an empty `path` over an unread class is a determined negative and must hedge: {v}");
assert_eq!(v["path"], serde_json::json!([]), "{v}");
for argv in [vec!["callers", "wrapper"], vec!["impact", "wrapper"], vec!["path", "top", "Fs"]] {
let mut args: Vec<&str> = argv.clone();
args.extend(["--report", &f.prefix]);
let out = Command::new(bin()).args(&args).output().expect("run candor-query");
let text = String::from_utf8(out.stdout).unwrap();
assert!(text.contains("INCOMPLETE") && text.contains("non-library-target"),
"{argv:?}: the prose channel must carry the note AND name the class: {text}");
}
let h = Fixture::new("cip-healthy");
h.write_report();
for argv in [vec!["callers", "inner"], vec!["impact", "inner"], vec!["path", "outer", "Fs"]] {
let mut args: Vec<&str> = argv.clone();
args.extend(["--report", &h.prefix, "--json"]);
let out = Command::new(bin()).args(&args).output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value =
serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert!(v.get("incomplete").is_none(),
"{argv:?}: a complete report gains NO caveat key: {v}");
let keys: Vec<&str> = v.as_object().unwrap().keys().map(String::as_str).collect();
assert!(keys.iter().all(|k| ["of", "direct", "transitive", "possibleViaUnknownDispatch",
"fn", "affectedCount", "affected", "entryPoints", "effect",
"path"].contains(k)),
"{argv:?}: the healthy document keeps its pinned key set exactly: {keys:?}");
}
for argv in [vec!["callers", "inner"], vec!["impact", "inner"], vec!["path", "outer", "Fs"]] {
let mut args: Vec<&str> = argv.clone();
args.extend(["--report", &h.prefix]);
let out = Command::new(bin()).args(&args).output().expect("run candor-query");
let text = String::from_utf8(out.stdout).unwrap();
assert!(!text.contains("INCOMPLETE"), "{argv:?}: healthy prose gains no note: {text}");
}
}
#[test]
fn advisory_verbs_answer_a_zero_rule_policy_with_the_caveat_document_at_an_unchanged_exit() {
let f = Fixture::new("zerorule");
f.write_report();
let zero = write_policy(&f, "zero.policy", "# no rules yet\n");
let entry_rule = format!("(entire policy {zero} — no rules parsed)");
for argv in [
vec!["whatif", "inner", "Net"],
vec!["fix", "inner", "Fs"],
vec!["fix-gate"],
vec!["fix-gate", "--strict"],
vec!["unverified"],
vec!["unverified", "--strict"],
] {
let mut args: Vec<&str> = argv.clone();
args.extend(["--report", &f.prefix, "--policy", &zero, "--json"]);
let out = Command::new(bin()).args(&args).output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0),
"{argv:?}: the caveat is a disclosure, not an exit code — exit UNCHANGED (0)");
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap())
.unwrap_or_else(|_| panic!("{argv:?} must emit a JSON caveat document on stdout"));
assert_eq!(v["unevaluated"][0]["rule"], serde_json::json!(entry_rule),
"{argv:?}: the whole-policy entry, the gate routes' own spelling: {v}");
for withheld in ["ok", "violations", "unverified", "remedies", "affected", "crossing"] {
assert!(v.get(withheld).is_none(),
"{argv:?}: result key `{withheld}` must be WITHHELD over a policy that asked nothing \
— an empty result is the prose ✓ in wire form: {v}");
}
}
let out = Command::new(bin())
.args(["gate", "--report", &f.prefix, "--policy", &zero, "--json"])
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(2), "the gate REFUSES a zero-rule policy");
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert_eq!(v["refused"], serde_json::json!(true));
assert_eq!(v["unevaluated"][0]["rule"], serde_json::json!(entry_rule),
"§6.2: the refusal's `unevaluated` carries one entry naming the whole policy — this route \
used to write the refusal with no `unevaluated` at all: {v}");
let real = write_policy(&f, "real.policy", "deny Net app\n");
for argv in [vec!["whatif", "inner", "Net"], vec!["fix-gate"], vec!["unverified"]] {
let mut args: Vec<&str> = argv.clone();
args.extend(["--report", &f.prefix, "--policy", &real, "--json"]);
let out = Command::new(bin()).args(&args).output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0), "{argv:?}: control exits 0");
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert!(v.get("ok").is_some(), "{argv:?}: control keeps `ok`: {v}");
assert!(v.get("unevaluated").is_none(), "{argv:?}: control gains no caveat: {v}");
}
}
#[test]
fn fix_json_pins_crossing_present_iff_answered_and_stdout_stays_pure_json() {
let f = Fixture::new("crossing");
f.write_report();
let deny = write_policy(&f, "deny.policy", "deny Fs\n");
let other = write_policy(&f, "other.policy", "deny Net elsewhere\n");
let out = Command::new(bin())
.args(["fix", "outer", "Fs", "--report", &f.prefix, "--policy", &deny, "--json"])
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert_eq!(v["crossing"], serde_json::json!(true), "a plan carries crossing: true: {v}");
assert!(v.get("deniedSpan").is_some(), "…beside the plan, not instead of it: {v}");
let out = Command::new(bin())
.args(["fix", "inner", "Fs", "--report", &f.prefix, "--policy", &other, "--json"])
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0), "the no-crossing arm's exit is unchanged (0)");
let stdout = String::from_utf8(out.stdout).unwrap();
let v: serde_json::Value = serde_json::from_str(&stdout).unwrap_or_else(|_| panic!(
"the no-crossing arm printed PROSE on a --json stdout (the §3.3.1 purity violation this \
ruling closes): {stdout}"));
assert_eq!(v["crossing"], serde_json::json!(false));
assert_eq!(v["reason"], serde_json::json!("not-forbidden"),
"the ts/swift reason token on the false arm: {v}");
let out = Command::new(bin())
.args(["fix", "inner", "Net", "--report", &f.prefix, "--policy", &deny, "--json"])
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert_eq!(v["crossing"], serde_json::json!(false));
assert_eq!(v["reason"], serde_json::json!("does-not-perform"), "{v}");
let g = Fixture::new("crossing-refused");
std::fs::write(format!("{}.app.scan.json", g.prefix),
r#"{"candor":{"version":"t","toolchain":"stable","spec":"0.28"},"package":"app",
"analyzed":{"count":1,"digest":"x"},
"functions":[{"fn":"app::noClass","loc":"s:1","inferred":["Net"],"direct":["Net"],"hosts":["h.example"],"hash":"a#n"}]}"#).unwrap();
let narrow = write_policy(&g, "narrow.policy", "deny Net[unknown-host] app\n");
let out = Command::new(bin())
.args(["fix", "app::noClass", "Net", "--report", &g.prefix, "--policy", &narrow, "--json"])
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert!(v.get("crossing").is_none(),
"a refused fix must carry NO crossing key — neither boolean is a statement there: {v}");
assert!(v.get("unevaluated").is_some(), "…and says which rule stopped it: {v}");
let out = Command::new(bin())
.args(["fix-gate", "--report", &f.prefix, "--policy", &deny, "--json"])
.output().expect("run candor-query");
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
let remedies = v["remedies"].as_array().expect("deny Fs over the Fs fixture yields a remedy");
assert!(!remedies.is_empty() && remedies.iter().all(|r| r.get("crossing").is_none()),
"`crossing` is `fix`'s key — a remedies entry must not gain it: {v}");
}
#[test]
fn gate_verdict_documents_carry_the_dropped_policy_lines_as_ignored() {
let f = Fixture::new("ignored");
f.write_report();
let dropped = write_policy(&f, "dropped.policy",
"# a comment\ndeny Fs\nfrobnicate the walrus # typo\nforbid glued->arrow\n");
let out = Command::new(bin())
.args(["gate", "--report", &f.prefix, "--policy", &dropped, "--json"])
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(1),
"deny Fs still fires — a dropped line never changes ok or the exit");
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
let ig = v["ignored"].as_array().unwrap_or_else(|| panic!(
"the verdict must carry the dropped lines as `ignored` — stderr is not the machine channel: {v}"));
assert_eq!(ig.len(), 2, "two dropped lines, the comment is not one: {v}");
assert_eq!(ig[0]["line"], serde_json::json!(3));
assert_eq!(ig[0]["text"], serde_json::json!("frobnicate the walrus # typo"),
"`text` is the source line VERBATIM, before comment-stripping: {v}");
assert!(ig[0]["reason"].as_str().unwrap().contains("unknown rule kind"), "{v}");
assert_eq!(ig[1]["line"], serde_json::json!(4));
assert!(!v["violations"].as_array().unwrap().is_empty(),
"the firing rule still fires beside the disclosure");
let parts = f.dir.join("parts.ndjson");
std::fs::write(&parts, "{\"rule\":\"AS-EFF-006\",\"fn\":\"f\",\"effects\":[\"Net\"],\"detail\":\"d\"}\n").unwrap();
let out = Command::new(bin())
.args(["gate-verdict", parts.to_string_lossy().as_ref(), "-", "--policy", &dropped])
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0));
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert_eq!(v["ignored"].as_array().map(Vec::len), Some(2),
"the assembled verdict route is not covered by its sibling: {v}");
let clean = write_policy(&f, "clean.policy", "deny Fs\n");
let out = Command::new(bin())
.args(["gate", "--report", &f.prefix, "--policy", &clean, "--json"])
.output().expect("run candor-query");
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert!(v.get("ignored").is_none(), "a clean policy's verdict stays byte-identical: {v}");
let out = Command::new(bin())
.args(["gate-verdict", parts.to_string_lossy().as_ref(), "-", "--policy", &clean])
.output().expect("run candor-query");
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert!(v.get("ignored").is_none(), "{v}");
}
fn write_excluded_report(f: &Fixture, excluded: &str, out_of_scope: Option<&str>) {
let oos = out_of_scope.map(|o| format!("\"outOfScope\": {o},\n ")).unwrap_or_default();
let report = format!(
r#"{{
"candor": {{ "version": "scan-test", "toolchain": "stable", "spec": "0.32" }},
"package": "rpt",
"analyzed": {{ "count": 1, "digest": "0000000000000000" }},
"excluded": {excluded},
{oos}"functions": [
{{ "fn": "inner", "loc": "src/lib.rs:2:1", "inferred": ["Fs"], "direct": ["Fs"], "hash": "rpt#inner", "paths": ["/x"] }}
]
}}"#
);
std::fs::write(format!("{}.rpt.scan.json", f.prefix), report).unwrap();
std::fs::write(format!("{}.rpt.scan.callgraph.json", f.prefix), r#"{"inner":[]}"#).unwrap();
}
#[test]
fn gate_report_refuses_a_deny_rule_over_classes_the_producer_never_read() {
let f = Fixture::new("unpeeked-noask");
let deny = write_policy(&f, "deny.policy", "deny Exec\n");
write_excluded_report(
&f,
r#"[{ "class": "build-script", "count": 1, "peeked": false, "reason": "compile time" }]"#,
None,
);
let out = Command::new(bin())
.args(["gate", "--report", &f.prefix, "--policy", &deny, "--json"])
.output().expect("run candor-query");
let err = String::from_utf8_lossy(&out.stderr).into_owned();
assert_eq!(out.status.code(), Some(2),
"a deny rule certified over a class NOBODY READ is the fail-open this row exists for — the \
producer's silence about the QUESTION is not an answer about the CODE. stderr: {err}");
assert!(err.contains("did not READ") && err.contains("build-script"),
"the refusal must NAME the unread class, or the operator cannot repair it: {err}");
let v: serde_json::Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert_eq!(v["ok"], serde_json::json!(false), "the DOCUMENT must agree with the exit: {v}");
assert_eq!(v["incomplete"], serde_json::json!(true),
"⟨0.32⟩ unread code makes the verdict INCOMPLETE, and a machine consumer reads that key, not \
the exit code: {v}");
let forbid = write_policy(&f, "forbid.policy", "forbid app -> infra\n");
let out = Command::new(bin())
.args(["gate", "--report", &f.prefix, "--policy", &forbid])
.output().expect("run candor-query");
let err = String::from_utf8_lossy(&out.stderr).into_owned();
assert!(!err.contains("did not READ"),
"a policy with no deny rule must not be refused for an unread class — that is the over-charge \
the scan route measured and carved out with `peek_attempted`: {err}");
write_excluded_report(&f, "[]", None);
let out = Command::new(bin())
.args(["gate", "--report", &f.prefix, "--policy", &deny, "--json"])
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0),
"an empty `excluded` is \"I excluded nothing\" — there is nothing unread to refuse over. \
stderr: {}", String::from_utf8_lossy(&out.stderr));
write_excluded_report(
&f,
r#"[{ "class": "build-script", "count": 1, "peeked": true, "reason": "compile time" }]"#,
Some("[]"),
);
let out = Command::new(bin())
.args(["gate", "--report", &f.prefix, "--policy", &deny])
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0),
"a class the producer READ and found clean must not be re-charged as unread: {}",
String::from_utf8_lossy(&out.stderr));
write_excluded_report(
&f,
r#"[{ "class": "build-output", "count": 1, "peeked": false, "judgedElsewhere": true, "reason": "derived" }]"#,
None,
);
let out = Command::new(bin())
.args(["gate", "--report", &f.prefix, "--policy", &deny])
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0),
"`judgedElsewhere` is the producer's statement that this class's SOURCE was judged; refusing \
over it double-charges one body of code: {}", String::from_utf8_lossy(&out.stderr));
}
#[test]
fn gate_report_refuses_a_corrupt_excluded_key_and_tolerates_an_absent_one() {
let f = Fixture::new("unpeeked-corrupt");
let deny = write_policy(&f, "deny.policy", "deny Exec\n");
write_excluded_report(&f, r#""oops""#, None);
let out = Command::new(bin())
.args(["gate", "--report", &f.prefix, "--policy", &deny])
.output().expect("run candor-query");
let err = String::from_utf8_lossy(&out.stderr).into_owned();
assert_eq!(out.status.code(), Some(2), "a corrupt key is corrupt input, never its empty value: {err}");
assert!(err.contains("`excluded`"), "the refusal must NAME the key it could not read: {err}");
f.write_report();
let out = Command::new(bin())
.args(["gate", "--report", &f.prefix, "--policy", &deny])
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0),
"ABSENT is ⟨0.26⟩'s cannot-answer and takes the documented default — refusing here would \
refuse every pre-⟨0.29⟩ report: {}", String::from_utf8_lossy(&out.stderr));
}
#[test]
fn advisory_verbs_refuse_a_deny_rule_over_classes_the_producer_never_read() {
let f = Fixture::new("unpeeked-advisory");
let deny = write_policy(&f, "deny.policy", "deny Exec\n");
write_excluded_report(
&f,
r#"[{ "class": "build-script", "count": 1, "peeked": false, "reason": "compile time" }]"#,
None,
);
for verb in ["fix-gate", "unverified"] {
let out = Command::new(bin())
.args([verb, "--report", &f.prefix, "--policy", &deny, "--strict", "--json"])
.output().expect("run candor-query");
let doc = String::from_utf8(out.stdout).unwrap();
let v: serde_json::Value = serde_json::from_str(&doc).unwrap();
assert_eq!(out.status.code(), Some(2),
"`{verb} --strict` must answer 2 wherever `gate --report` does over the same bytes \
(SPEC §3.2) — the gate refuses this report for an unread class. doc: {doc}");
assert_eq!(v["incomplete"], serde_json::json!(true),
"the DOCUMENT must carry the same finding as the exit — a CI wrapper reads the exit, an \
agent reads this: {doc}");
assert!(v.get("ok").is_none(),
"`ok` must be WITHHELD over an incomplete universe, not set false: {doc}");
}
let out = Command::new(bin())
.args(["fix", "inner", "Exec", "--report", &f.prefix, "--policy", &deny, "--json"])
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(0), "`fix` answers no `ok`, so its exit must not move");
let doc = String::from_utf8(out.stdout).unwrap();
let v: serde_json::Value = serde_json::from_str(&doc).unwrap();
assert_eq!(v["incomplete"], serde_json::json!(true),
"`fix`'s remedy is computed over a universe it cannot fully see, and the document must say so: {doc}");
}
#[test]
fn advisory_verbs_do_not_refuse_for_a_peek_nobody_asked_for() {
let f = Fixture::new("unpeeked-advisory-controls");
let deny = write_policy(&f, "deny.policy", "deny Exec\n");
write_excluded_report(
&f,
r#"[{ "class": "build-script", "count": 1, "peeked": true, "reason": "compile time" }]"#,
Some("[]"),
);
for verb in ["fix-gate", "unverified"] {
let out = Command::new(bin())
.args([verb, "--report", &f.prefix, "--policy", &deny, "--strict", "--json"])
.output().expect("run candor-query");
let doc = String::from_utf8(out.stdout).unwrap();
let v: serde_json::Value = serde_json::from_str(&doc).unwrap();
assert_eq!(out.status.code(), Some(0),
"a class the producer READ and found clean is not unread code — re-charging it would \
refuse every complete report with an exclusion in it. `{verb}` doc: {doc}");
assert_eq!(v["ok"], serde_json::json!(true),
"`{verb}` must still certify a report whose every class was peeked: {doc}");
assert!(v.get("incomplete").is_none(), "no hedge is owed here: {doc}");
}
write_excluded_report(
&f,
r#"[{ "class": "build-output", "count": 1, "peeked": false, "judgedElsewhere": true, "reason": "derived" }]"#,
None,
);
for verb in ["fix-gate", "unverified"] {
let out = Command::new(bin())
.args([verb, "--report", &f.prefix, "--policy", &deny, "--strict", "--json"])
.output().expect("run candor-query");
let doc = String::from_utf8(out.stdout).unwrap();
assert_eq!(out.status.code(), Some(0),
"`judgedElsewhere` carves the class out on this route exactly as it does on the gate's. \
`{verb}` doc: {doc}");
}
write_excluded_report(
&f,
r#"[{ "class": "build-script", "count": 1, "peeked": false, "reason": "compile time" }]"#,
None,
);
let forbid = write_policy(&f, "forbid.policy", "forbid app -> infra\n");
for verb in ["fix-gate", "unverified"] {
let out = Command::new(bin())
.args([verb, "--report", &f.prefix, "--policy", &forbid, "--strict", "--json"])
.output().expect("run candor-query");
let doc = String::from_utf8(out.stdout).unwrap();
let v: serde_json::Value = serde_json::from_str(&doc).unwrap();
assert!(v.get("unevaluated").is_some(),
"instrument: this row means nothing unless the forbid rule really is unanswerable here — \
`{verb}` doc: {doc}");
assert!(v.get("incomplete").is_none(),
"a policy with no deny rule asks nothing of the unread class, so no incompleteness is \
owed — this is the over-charge the scan route carved out with `peek_attempted`. \
`{verb}` doc: {doc}");
}
let pure = write_policy(&f, "pure.policy", "pure\n");
for verb in ["fix-gate", "unverified"] {
let out = Command::new(bin())
.args([verb, "--report", &f.prefix, "--policy", &pure, "--strict", "--json"])
.output().expect("run candor-query");
let doc = String::from_utf8(out.stdout).unwrap();
let v: serde_json::Value = serde_json::from_str(&doc).unwrap();
assert_eq!(out.status.code(), Some(2),
"`pure` is a deny rule with an empty effect list — the rung must not be disarmed by the \
strictest policy there is. `{verb}` doc: {doc}");
assert_eq!(v["incomplete"], serde_json::json!(true),
"`pure` must arm the unread-class cause in the DOCUMENT, not only reach a non-zero exit by \
some other route. `{verb}` doc: {doc}");
}
}
fn write_two_package_reports(f: &Fixture, func: &str, effects: &str) {
for pkg in ["a", "b"] {
let report = format!(
r#"{{
"candor": {{ "version": "scan-test", "toolchain": "stable", "spec": "0.32" }},
"package": "{pkg}",
"analyzed": {{ "count": 1, "digest": "0000000000000000" }},
"functions": [
{{ "fn": "{func}", "loc": "src/lib.rs:1:1", "inferred": {effects}, "direct": {effects}, "hash": "{pkg}#{func}", "cmds": ["curl"] }}
]
}}"#
);
std::fs::write(format!("{}.{pkg}.scan.json", f.prefix), report).unwrap();
std::fs::write(format!("{}.{pkg}.scan.callgraph.json", f.prefix), format!(r#"{{"{func}":[]}}"#)).unwrap();
}
}
#[test]
fn a_verdict_row_names_the_unit_it_is_about() {
let f = Fixture::new("verdict-identity");
let deny = write_policy(&f, "deny.policy", "deny Exec\n");
write_two_package_reports(&f, "go", r#"["Exec"]"#);
let out = Command::new(bin())
.args(["gate", "--report", &f.prefix, "--policy", &deny, "--json"])
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(1), "both members violate `deny Exec`");
let doc = String::from_utf8(out.stdout).unwrap();
let v: serde_json::Value = serde_json::from_str(&doc).unwrap();
let rows = v["violations"].as_array().unwrap();
assert_eq!(rows.len(), 2, "instrument: this row means nothing without TWO violations: {doc}");
assert_ne!(rows[0], rows[1],
"two units, two rows, and NOTHING in either says which unit it is about — a reader cannot tell \
two broken members from one listed twice: {doc}");
let hashes: Vec<&str> = rows.iter().map(|r| r["hash"].as_str().unwrap_or("")).collect();
assert_eq!(hashes, vec!["a#go", "b#go"],
"each row must carry its unit's `hash` — §2.2's join key — and in SORTED order, because the \
document's order is part of §3.3.1 byte-equality and `(rule, detail)` ties on these twins: {doc}");
}
#[test]
fn a_single_unit_verdict_keeps_every_key_it_had() {
let f = Fixture::new("verdict-identity-single");
let deny = write_policy(&f, "deny.policy", "deny Fs\n");
f.write_report();
let out = Command::new(bin())
.args(["gate", "--report", &f.prefix, "--policy", &deny, "--json"])
.output().expect("run candor-query");
assert_eq!(out.status.code(), Some(1), "`deny Fs` fires on this report");
let doc = String::from_utf8(out.stdout).unwrap();
let v: serde_json::Value = serde_json::from_str(&doc).unwrap();
let rows = v["violations"].as_array().unwrap();
assert_eq!(rows.len(), 2, "instrument: two functions violate here: {doc}");
for r in rows {
let o = r.as_object().unwrap();
assert_eq!(o["rule"], serde_json::json!("AS-EFF-006"), "{doc}");
assert!(o.contains_key("fn") && o.contains_key("effects") && o.contains_key("detail"),
"the pre-⟨0.32⟩ keys must all survive: {doc}");
let mut keys: Vec<&str> = o.keys().map(String::as_str).collect();
keys.sort_unstable();
assert_eq!(keys, vec!["detail", "effects", "fn", "hash", "rule"],
"a verdict row gains IDENTITY and nothing else: {doc}");
}
let names: Vec<&str> = rows.iter().map(|r| r["fn"].as_str().unwrap()).collect();
assert_eq!(names, vec!["inner", "outer"], "the `fn` field stays the bare NAME: {doc}");
}