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"], "0.27");
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 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 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 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 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 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:#}");
}