use super::*;
#[test]
fn embedded_agents_contract_matches_the_repo_doc() {
let embedded = include_str!("../AGENTS.md");
match std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/../../AGENTS.md")) {
Ok(root) if root.contains("instructions for an AI coding agent") => {
assert_eq!(embedded, root, "crate AGENTS.md drifted from the repo root — re-copy it");
}
_ => { }
}
}
#[test]
fn rewire_flags_dropped_edges_not_added_ones() {
let cg = |pairs: &[(&str, &[&str])]| -> BTreeMap<String, Vec<String>> {
pairs.iter().map(|(k, v)| (k.to_string(), v.iter().map(|s| s.to_string()).collect())).collect()
};
let base = cg(&[("api::handle", &["service::place_order"]), ("cart::total", &["pricing::quote"])]);
let cur = cg(&[("cart::total", &["pricing::quote"]), ("main", &["api::handle"])]);
let d = dropped_edges(&cur, &base);
assert_eq!(d.get("api::handle"), Some(&vec!["service::place_order"])); assert!(!d.contains_key("cart::total")); assert!(!d.contains_key("main")); assert!(dropped_edges(&base, &base).is_empty());
}
#[test]
fn gains_flags_a_supply_chain_capability_gain() {
let set = |xs: &[&str]| xs.iter().map(|s| s.to_string()).collect::<BTreeSet<String>>();
assert_eq!(gained_effects(&set(&["Fs", "Net"]), &set(&["Fs"])), vec!["Net"]); assert!(gained_effects(&set(&["Fs"]), &set(&["Fs"])).is_empty()); assert!(gained_effects(&set(&["Fs"]), &set(&["Fs", "Net"])).is_empty()); }
#[test]
fn match_tier_ladder() {
assert_eq!(match_tier("pricing::Pricing::quote", "pricing::Pricing::quote"), 3);
assert_eq!(match_tier("pricing::Pricing::quote", "Pricing::quote"), 2);
assert_eq!(match_tier("pricing::Pricing::quote", "quote"), 2);
assert_eq!(match_tier("pricing::Pricing::quote_bulk", "quote"), 1); assert_eq!(match_tier("pricing::Pricing::quote", "ricing::quote"), 1); assert_eq!(match_tier("pricing::Pricing::quote", "zzz"), 0);
let names = ["pricing::Pricing::quote", "pricing::Pricing::quote_bulk"];
let t = best_tier(names.iter().copied(), "Pricing::quote");
assert_eq!(t, 2);
assert!(q_match(names[0], "Pricing::quote", t));
assert!(!q_match(names[1], "Pricing::quote", t));
let t2 = best_tier(names.iter().copied(), "ricing");
assert_eq!(t2, 1);
assert!(q_match(names[0], "ricing", t2) && q_match(names[1], "ricing", t2));
}
#[test]
fn whatif_scope_and_policy_parse() {
use candor_classify::policy::{parse_policy, scope_matches};
assert!(scope_matches("app::domain::handle", "domain"));
assert!(scope_matches("crate::domain_logic", "domain")); assert!(!scope_matches("app::subdomain::handle", "domain")); assert!(scope_matches("api::handle", "api"));
let p = parse_policy("deny Net Db api\npure parse\ndeny Exec\nallow Net in billing x\nforbid a -> b\n# c");
let rules = &p.rules;
assert_eq!(rules.len(), 3);
assert_eq!(rules[0].effects.iter().copied().collect::<Vec<_>>(), vec!["Db", "Net"]);
assert_eq!(rules[0].scope.as_deref(), Some("api"));
assert!(rules[1].effects.is_empty()); assert_eq!(rules[1].scope.as_deref(), Some("parse"));
assert_eq!(rules[2].scope, None); assert_eq!(p.allow_rules.len(), 1);
assert_eq!(p.layer_rules.len(), 1);
}
#[test]
fn containment_layer_derivation() {
let names: Vec<String> = ["pgman::conn::connect", "pgman::query::run", "pgman::main"]
.iter()
.map(|s| s.to_string())
.collect();
let refs: Vec<&String> = names.iter().collect();
let pl = common_prefix_len(&refs);
assert_eq!(pl, 1, "shared root is `pgman`");
assert_eq!(layer_of("pgman::conn::connect", pl), "conn");
assert_eq!(layer_of("pgman::query::Q::run", pl), "query");
assert_eq!(layer_of("pgman::main", pl), "(root)");
assert_eq!(norm_name("<pgman::conn::Conn as std::fmt::Debug>::fmt"), "pgman::conn::Conn");
assert_eq!(layer_of("<pgman::conn::Conn as std::fmt::Debug>::fmt", pl), "conn");
let multi: Vec<String> =
["a::x::f".to_string(), "b::y::g".to_string()].to_vec();
let mrefs: Vec<&String> = multi.iter().collect();
assert_eq!(common_prefix_len(&mrefs), 0);
assert_eq!(layer_of("a::x::f", 0), "a");
}
#[test]
fn is_scan_artifact_discriminates() {
assert!(is_scan_artifact("report", "report.mycrate.scan.json"));
assert!(is_scan_artifact("report", "report.mycrate.scan.callgraph.json"));
assert!(!is_scan_artifact("report", "report.mycrate.Rlib.json"));
assert!(!is_scan_artifact("report", "report.mycrate.Executable.callgraph.json"));
assert!(!is_scan_artifact("report", "report.calibrated.json"));
assert!(!is_scan_artifact("report", "other.mycrate.scan.json"));
}
#[test]
fn report_backend_and_clear_other() {
let dir = std::env::temp_dir().join("candor-query-backend-test");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let pre = dir.join("report");
let prefix = pre.to_string_lossy().to_string();
let w = |name: &str| std::fs::write(dir.join(name), b"{}").unwrap();
assert_eq!(report_backend(&prefix), "none");
w("report.c.scan.json");
w("report.c.scan.callgraph.json");
assert_eq!(report_backend(&prefix), "scan");
w("report.c.Rlib.json");
w("report.c.Rlib.callgraph.json");
w("report.calibrated.json");
assert_eq!(report_backend(&prefix), "scan"); let removed = clear_other_reports(&prefix, "lint"); assert_eq!(removed, 2); assert!(!dir.join("report.c.scan.json").exists());
assert!(dir.join("report.c.Rlib.json").exists());
assert_eq!(report_backend(&prefix), "lint");
w("report.c.scan.json");
let removed = clear_other_reports(&prefix, "scan");
assert!(removed >= 3); assert!(dir.join("report.c.scan.json").exists());
assert_eq!(report_backend(&prefix), "scan");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn locate_picks_newest_by_mtime() {
use std::time::{Duration, SystemTime};
let dir = std::env::temp_dir().join("candor-query-locate-test");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let old = dir.join("libcandor@nightly-2025-01-01-x.dylib");
let new = dir.join("libcandor@nightly-2026-01-01-x.dylib");
std::fs::write(&old, b"x").unwrap();
std::fs::write(&new, b"x").unwrap();
let f_old = std::fs::OpenOptions::new().write(true).open(&old).unwrap();
f_old.set_modified(SystemTime::now() - Duration::from_secs(100)).unwrap();
let f_new = std::fs::OpenOptions::new().write(true).open(&new).unwrap();
f_new.set_modified(SystemTime::now()).unwrap();
let out = locate_newest("lib", &[dir.to_string_lossy().to_string()]);
assert_eq!(out, Some(new)); let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn merge_hook_is_nondestructive_and_idempotent() {
let dir = std::env::temp_dir().join("candor-query-merge-test");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let cmd = "X/stop-hook.sh".to_string();
let arg = |p: &std::path::Path| vec![p.to_string_lossy().to_string(), cmd.clone()];
let fresh = dir.join("fresh.json");
assert_eq!(cmd_merge_hook(&arg(&fresh)), 0);
let v: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&fresh).unwrap()).unwrap();
assert_eq!(v["hooks"]["Stop"][0]["hooks"][0]["command"], cmd.as_str());
let keep = dir.join("keep.json");
std::fs::write(&keep, r#"{"model":"opus","permissions":{"allow":["Bash"]}}"#).unwrap();
assert_eq!(cmd_merge_hook(&arg(&keep)), 0);
let v: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&keep).unwrap()).unwrap();
assert_eq!(v["model"], "opus");
assert_eq!(v["permissions"]["allow"][0], "Bash");
assert_eq!(v["hooks"]["Stop"][0]["hooks"][0]["command"], cmd.as_str());
assert_eq!(cmd_merge_hook(&arg(&keep)), 0);
let v: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&keep).unwrap()).unwrap();
assert_eq!(v["hooks"]["Stop"].as_array().unwrap().len(), 1);
let bad = dir.join("bad.json");
let original = "{ // comment\n \"model\": \"x\",\n}";
std::fs::write(&bad, original).unwrap();
assert_eq!(cmd_merge_hook(&arg(&bad)), 0);
assert_eq!(std::fs::read_to_string(&bad).unwrap(), original, "must not touch a non-JSON file");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn state_hash_is_deterministic_and_scoped() {
let dir = std::env::temp_dir().join("candor-query-state-test");
let _ = std::fs::remove_dir_all(&dir);
for d in ["src", "sub", "target/x", ".git"] {
std::fs::create_dir_all(dir.join(d)).unwrap();
}
std::fs::write(dir.join("src/a.rs"), "fn a(){}").unwrap();
std::fs::write(dir.join("sub/b.rs"), "fn b(){}").unwrap();
std::fs::write(dir.join("target/x/c.rs"), "fn c(){}").unwrap(); std::fs::write(dir.join(".git/d.rs"), "fn d(){}").unwrap(); std::fs::write(dir.join("src/notrust.txt"), "ignored").unwrap();
let hash = |root: &Path| -> u64 {
let mut files = Vec::new();
collect_rs(root, &mut files);
files.sort();
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for f in &files {
for &b in f.strip_prefix(root).unwrap_or(f).to_string_lossy().as_bytes() {
h ^= b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
for &b in std::fs::read(f).unwrap_or_default().iter() {
h ^= b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
}
h
};
let mut files = Vec::new();
collect_rs(&dir, &mut files);
assert_eq!(files.len(), 2, "must collect exactly src/a.rs + sub/b.rs");
let h1 = hash(&dir);
assert_eq!(h1, hash(&dir), "deterministic");
std::fs::write(dir.join("target/x/c.rs"), "fn c(){ let _=9; }").unwrap();
std::fs::write(dir.join(".git/d.rs"), "fn d(){ let _=9; }").unwrap();
assert_eq!(h1, hash(&dir), "target/ and .git/ edits are ignored");
std::fs::write(dir.join("src/a.rs"), "fn a(){ let _=1; }").unwrap();
assert_ne!(h1, hash(&dir), "a real .rs edit changes the hash");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn globs_discriminate_reports_from_sidecars() {
let dir = std::env::temp_dir().join("candor-query-glob-test");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
for f in [
"report.mycrate.lib.json", "report.mycrate.Executable.json", "report.calibrated.json", "report.encountered-mycrate.json", "report.single.json", "other.a.b.json", ] {
std::fs::write(dir.join(f), "[]").unwrap();
}
let prefix = dir.join("report");
let prefix = prefix.to_str().unwrap();
let reports: Vec<String> =
glob_reports(prefix).iter().map(|p| p.file_name().unwrap().to_str().unwrap().to_string()).collect();
assert_eq!(reports, vec!["report.mycrate.Executable.json", "report.mycrate.lib.json"]);
let enc: Vec<String> =
glob_encountered(prefix).iter().map(|p| p.file_name().unwrap().to_str().unwrap().to_string()).collect();
assert_eq!(enc, vec!["report.encountered-mycrate.json"]);
assert_eq!(prefix_base(prefix), "report");
let _ = std::fs::remove_dir_all(&dir);
}
fn comp_fixture(name: &str, envelope: serde_json::Value) -> String {
let dir = std::env::temp_dir().join(format!("candor-query-completeness-{name}"));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let rep = dir.join("rep.fixture.scan.json");
std::fs::write(&rep, envelope.to_string()).unwrap();
rep.to_str().unwrap().to_string()
}
#[test]
fn a_complete_report_discloses_nothing_on_either_channel() {
let rep = comp_fixture(
"complete",
serde_json::json!({
"candor": "0.28",
"analyzed": { "count": 2 },
"functions": [{ "fn": "f", "inferred": ["Fs"], "direct": ["Fs"] }],
}),
);
let comp = crate::completeness::report_completeness(&rep);
assert!(!comp.incomplete(), "a complete report is not incomplete");
assert!(!comp.must_hedge(), "a complete report has nothing to hedge about");
assert!(comp.fields().is_none(), "no flattened disclosure on a complete report");
let mut doc = serde_json::json!({ "effect": "Fs", "directly": [], "inherited": [] });
let before = serde_json::to_string(&doc).unwrap();
comp.write_json(&mut doc);
assert_eq!(serde_json::to_string(&doc).unwrap(), before, "write_json is not a no-op");
let mut note = Vec::new();
comp.write_note_for_test(&mut note, "x", "y");
assert!(note.is_empty(), "print_note is not a no-op");
}
#[test]
fn judged_nothing_hedges_the_answer_without_touching_the_exit_code() {
let rep = comp_fixture(
"judged-nothing",
serde_json::json!({ "candor": "0.28", "analyzed": { "count": 0 }, "functions": [] }),
);
let comp = crate::completeness::report_completeness(&rep);
assert!(!comp.incomplete(), "count-0 must NOT reach the exit-code predicate");
assert!(comp.must_hedge(), "count-0 must reach the disclosure predicate");
let mut doc = serde_json::json!({ "reaches": [] });
comp.write_json(&mut doc);
assert_eq!(doc["incomplete"], serde_json::json!(true));
assert_eq!(doc["judgedNothing"].as_array().unwrap().len(), 1);
assert!(doc.get("unanalyzed").is_none(), "there is no unread FILE in the count-0 row");
assert!(comp.gate_line().contains("exits 0"), "count-0 must not claim the gate refuses");
let rep = comp_fixture(
"unanalyzed",
serde_json::json!({
"candor": "0.28",
"analyzed": { "count": 3 },
"unanalyzed": [{ "path": "src/a.rs", "reason": "parse error" }],
"functions": [{ "fn": "f", "inferred": ["Fs"], "direct": ["Fs"] }],
}),
);
let comp = crate::completeness::report_completeness(&rep);
assert!(comp.incomplete() && comp.must_hedge());
assert!(comp.gate_line().contains("exits 2"));
let mut doc = serde_json::json!({ "reaches": [] });
comp.write_json(&mut doc);
assert_eq!(doc["unanalyzed"].as_array().unwrap().len(), 1);
assert!(doc.get("judgedNothing").is_none(), "a report with 3 analyzed units judged something");
}
#[test]
fn a_report_with_no_analyzed_manifest_is_row_three_not_row_one() {
let rep = comp_fixture(
"no-manifest",
serde_json::json!({ "candor": "0.20", "functions": [] }),
);
let comp = crate::completeness::report_completeness(&rep);
assert!(!comp.incomplete(), "row 3 must NOT reach the exit-code predicate — the gate exits 0");
assert!(comp.must_hedge(), "row 3 must reach the disclosure predicate — no manifest, no claim");
let mut doc = serde_json::json!({ "reaches": [] });
comp.write_json(&mut doc);
assert_eq!(doc["incomplete"], serde_json::json!(true));
assert_eq!(doc["noManifest"], serde_json::json!([rep]),
"SPEC §2 pins `noManifest: [\"<report path>\", …]` verbatim: {doc}");
assert!(doc.get("judgedNothing").is_none(),
"a row-3 report DECLARES nothing — saying it declared `analyzed.count: 0` is a FALSE \
disclosure, and one key meaning two things loses the distinction §2's table draws: {doc}");
let mut note = Vec::new();
comp.write_note_for_test(&mut note, "x", "y");
let note = String::from_utf8(note).unwrap();
assert!(note.contains("NO `analyzed` manifest"), "the prose must name the real cause: {note}");
assert!(!note.contains("analyzed.count: 0") && !note.contains("JUDGED NOTHING"),
"…and must not send the reader to row 1's repair: {note}");
let rep1 = comp_fixture(
"no-manifest-row1",
serde_json::json!({ "candor": "0.28", "analyzed": { "count": 0 }, "functions": [] }),
);
let c1 = crate::completeness::report_completeness(&rep1);
let mut d1 = serde_json::json!({ "reaches": [] });
c1.write_json(&mut d1);
assert_eq!(d1["judgedNothing"], serde_json::json!([rep1]), "{d1}");
assert!(d1.get("noManifest").is_none(), "the split goes both ways or it is a rename: {d1}");
let rep2 = comp_fixture(
"no-manifest-row2",
serde_json::json!({ "candor": "0.28", "analyzed": { "count": 7 }, "functions": [] }),
);
let c2 = crate::completeness::report_completeness(&rep2);
assert!(!c2.must_hedge(), "row 2 is a legitimate all-pure claim §2 rule 3 requires a consumer \
to BELIEVE — hedging all three rows disables the feature");
assert!(c2.fields().is_none());
let rep3 = comp_fixture(
"no-manifest-with-entries",
serde_json::json!({
"candor": "0.20",
"functions": [{ "fn": "f", "inferred": ["Fs"], "direct": ["Fs"] }],
}),
);
let c3 = crate::completeness::report_completeness(&rep3);
assert!(!c3.must_hedge(), "a manifest-less report that LISTS entries is not hedging at all");
let dir = std::env::temp_dir().join("candor-query-completeness-no-manifest-both");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("rep.aa.scan.json"),
r#"{"candor":"0.28","analyzed":{"count":0},"functions":[]}"#).unwrap();
std::fs::write(dir.join("rep.bb.scan.json"), r#"{"candor":"0.20","functions":[]}"#).unwrap();
let both = crate::completeness::report_completeness(dir.join("rep").to_str().unwrap());
let mut db = serde_json::json!({ "reaches": [] });
both.write_json(&mut db);
assert_eq!(db["judgedNothing"].as_array().unwrap().len(), 1, "{db}");
assert_eq!(db["noManifest"].as_array().unwrap().len(), 1, "{db}");
assert!(db["judgedNothing"][0].as_str().unwrap().ends_with("rep.aa.scan.json"), "{db}");
assert!(db["noManifest"][0].as_str().unwrap().ends_with("rep.bb.scan.json"), "{db}");
}
#[test]
fn an_unread_exclusion_class_hedges_the_answer_without_touching_the_exit_code() {
let unread = serde_json::json!({
"candor": "0.32",
"analyzed": { "count": 1 },
"excluded": [{ "class": "build-script", "count": 1, "peeked": false, "reason": "r" }],
"functions": [{ "fn": "f", "inferred": ["Fs"], "direct": ["Fs"] }],
});
let rep = comp_fixture("unread-class", unread.clone());
let comp = crate::completeness::report_completeness(&rep);
assert!(comp.must_hedge(), "a class the scan never opened must reach the disclosure predicate");
assert!(
!comp.incomplete(),
"…and MUST NOT reach the exit-code predicate: `unverified --strict` computes its 2 from \
this, and over a report the gate — holding no deny rule — exits 0 on, a verb exiting 2 \
would claim it got LESS far than the gate. ⟨0.24⟩'s over-claim, mirrored."
);
let mut doc = serde_json::json!({ "reaches": [] });
comp.write_json(&mut doc);
assert_eq!(doc["incomplete"], serde_json::json!(true), "{doc}");
assert!(doc.get("unread").is_none(), "the descriptive hedge mints no wire key: {doc}");
let mut note = Vec::new();
comp.write_note_for_test(&mut note, "so-what", "tail");
let note = String::from_utf8(note).unwrap();
assert!(note.contains("exclusion class(es) the scan did NOT READ"), "{note}");
assert!(note.contains("build-script"), "the class must be NAMED: {note}");
assert!(comp.gate_line().contains("under any policy it can evaluate"), "{}", comp.gate_line());
let mut peeked = unread.clone();
peeked["excluded"][0]["peeked"] = serde_json::json!(true);
let c = crate::completeness::report_completeness(&comp_fixture("peeked-class", peeked));
assert!(!c.must_hedge(), "a class the peek READ is not an unread class");
assert!(c.fields().is_none());
let mut elsewhere = unread;
elsewhere["excluded"][0]["judgedElsewhere"] = serde_json::json!(true);
let c = crate::completeness::report_completeness(&comp_fixture("elsewhere-class", elsewhere));
assert!(!c.must_hedge(), "`judgedElsewhere: true` is judged, not unread");
}
#[test]
fn the_row_three_split_does_not_move_the_coverage_predicate() {
let row3 = r#"{"candor":"0.20","package":"legacy","functions":[]}"#;
assert!(candor_report::report_judged_nothing(row3),
"an absent manifest must STILL grant no coverage — row 3 is `no manifest, no claim`");
assert!(candor_report::report_has_no_manifest(row3), "…and it is row 3, not row 1");
let row1 = r#"{"candor":"0.28","package":"facade","analyzed":{"count":0},"functions":[]}"#;
assert!(candor_report::report_judged_nothing(row1));
assert!(!candor_report::report_has_no_manifest(row1), "row 1 HAS a manifest; it declares 0");
let row2 = r#"{"candor":"0.28","package":"pure","analyzed":{"count":7},"functions":[]}"#;
assert!(!candor_report::report_judged_nothing(row2), "the row-2 control: a believed all-pure claim");
assert!(!candor_report::report_has_no_manifest(row2));
let row3_full = r#"{"candor":"0.20","package":"legacy","functions":[{"fn":"f","inferred":["Fs"]}]}"#;
assert!(!candor_report::report_judged_nothing(row3_full));
assert!(candor_report::report_has_no_manifest(row3_full));
assert!(candor_report::report_judged_nothing("{not json"));
assert!(!candor_report::report_has_no_manifest("{not json"));
}
fn refuse_beside(rep_path: &str, reason: &str) {
let rep = std::path::Path::new(rep_path);
let dir = std::fs::canonicalize(rep.parent().unwrap()).unwrap();
let prefix = dir.join("rep");
std::fs::write(
dir.join("rep.refused.json"),
serde_json::json!({
"refused": true,
"prefix": prefix.to_str().unwrap(),
"target": ".",
"reason": reason,
})
.to_string(),
)
.unwrap();
}
#[test]
fn a_refused_prefix_reaches_incomplete_and_must_hedge_and_both_channels() {
let rep = comp_fixture(
"refused",
serde_json::json!({
"candor": "0.34",
"analyzed": { "count": 1 },
"functions": [{ "fn": "f", "inferred": ["Fs"], "direct": ["Fs"] }],
}),
);
let clean = crate::completeness::report_completeness(&rep);
assert!(!clean.incomplete() && !clean.must_hedge(), "no marker yet — must read as complete");
refuse_beside(&rep, "R152 unit test: simulated refusal");
let comp = crate::completeness::report_completeness(&rep);
assert!(comp.incomplete(), "a refused prefix must reach the EXIT-CODE predicate — `gate \
--report` refuses over it unconditionally, so `--strict` must too");
assert!(comp.must_hedge());
assert_eq!(comp.gate_line(), "`gate --report` exits 2 over these bytes.");
let mut doc = serde_json::json!({ "reaches": [] });
comp.write_json(&mut doc);
assert_eq!(doc["incomplete"], serde_json::json!(true));
assert_eq!(doc["refused"], serde_json::json!(["R152 unit test: simulated refusal"]), "{doc}");
let mut note = Vec::new();
comp.write_note_for_test(&mut note, "so-what", "tail");
let note = String::from_utf8(note).unwrap();
assert!(note.contains("REFUSED"), "{note}");
assert!(note.contains("R152 unit test: simulated refusal"), "{note}");
}
#[test]
fn absorb_unions_a_refusal_from_either_locator() {
let cur = comp_fixture("refused-absorb-cur", serde_json::json!({ "candor": "0.34", "analyzed": { "count": 1 }, "functions": [] }));
let base = comp_fixture("refused-absorb-base", serde_json::json!({ "candor": "0.34", "analyzed": { "count": 1 }, "functions": [] }));
refuse_beside(&base, "baseline refused");
let mut c = crate::completeness::report_completeness(&cur);
assert!(!c.incomplete(), "the current side alone carries no marker");
c.absorb(crate::completeness::report_completeness(&base));
assert!(c.incomplete(), "a refused BASELINE must disqualify the union — `containment`'s answer \
is a DIFFERENCE, unsound if either side is disowned");
assert_eq!(c.refused.len(), 1);
}
#[test]
fn unverified_strict_refuses_over_a_refused_report() {
let (rep, pol) = unv_fixture(
"refused",
serde_json::json!([{ "fn": "f", "inferred": [], "direct": [] }]),
"deny Net\n",
);
assert_eq!(unv(&rep, &pol, None), 0, "CONTROL: no marker, no Unknown holes — clean");
refuse_beside(&rep, "R152: unverified must refuse over a refused report");
assert_eq!(unv(&rep, &pol, None), 2, "`gate --report` refuses (exit 2) over these bytes \
unconditionally; `--strict` must match it, not answer \
the pre-⟨R152⟩ 0 (\"PROVABLY clean ✓\")");
}
#[test]
fn fix_gate_strict_refuses_over_a_refused_report() {
let dir = std::env::temp_dir().join("candor-query-fixgate-refused");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let rep = dir.join("rep.fixture.scan.json");
let pol = dir.join("candor.policy");
std::fs::write(
&rep,
serde_json::json!({
"candor": "0.34",
"analyzed": { "count": 1 },
"functions": [{ "fn": "f", "inferred": [], "direct": [] }],
})
.to_string(),
)
.unwrap();
std::fs::write(&pol, "deny Net\n").unwrap();
let rep = rep.to_str().unwrap().to_string();
let pol = pol.to_str().unwrap().to_string();
let args = |rep: &str, pol: &str| -> Vec<String> {
vec!["--report".into(), rep.into(), "--policy".into(), pol.into(), "--strict".into()]
};
assert_eq!(
crate::fix::cmd_fix_gate(&args(&rep, &pol)), 0,
"CONTROL: no marker, no crossings — clean"
);
refuse_beside(&rep, "R152: fix-gate must refuse over a refused report");
assert_eq!(
crate::fix::cmd_fix_gate(&args(&rep, &pol)), 2,
"`gate --report` refuses (exit 2) over these bytes unconditionally; `--strict` must match \
it, not answer the pre-⟨R152⟩ 0 (\"no deny/pure boundary crossings in this report ✓\")"
);
}
#[test]
fn a_refused_prefix_does_not_invent_a_new_exit_family_for_descriptive_verbs() {
let rep = comp_fixture(
"refused-descriptive",
serde_json::json!({ "candor": "0.34", "analyzed": { "count": 1 }, "functions": [] }),
);
refuse_beside(&rep, "R152: descriptive verbs disclose, they do not refuse");
let comp = crate::completeness::report_completeness(&rep);
assert!(comp.incomplete() && comp.must_hedge(), "the cause is present");
}
fn unv_fixture(name: &str, entries: serde_json::Value, policy: &str) -> (String, String) {
let dir = std::env::temp_dir().join(format!("candor-query-unverified-{name}"));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let rep = dir.join("rep.fixture.scan.json");
let pol = dir.join("candor.policy");
std::fs::write(&rep, serde_json::json!({ "candor": "0.24", "functions": entries }).to_string())
.unwrap();
std::fs::write(&pol, policy).unwrap();
(rep.to_str().unwrap().to_string(), pol.to_str().unwrap().to_string())
}
fn unv(rep: &str, pol: &str, class: Option<&str>) -> i32 {
let mut args: Vec<String> = vec![
"--report".into(), rep.into(), "--policy".into(), pol.into(), "--strict".into(),
];
if let Some(c) = class {
args.push("--class".into());
args.push(c.into());
}
crate::unverified::cmd_unverified(&args)
}
#[test]
fn unverified_class_resolves_the_reason_transitively() {
let (rep, pol) = unv_fixture(
"transitive",
serde_json::json!([
{ "fn": "infra::dial", "inferred": ["Unknown"], "direct": ["Unknown"],
"unknownWhy": ["dispatch:Port"] },
{ "fn": "domain::price", "inferred": ["Unknown"], "calls": ["infra::dial"] },
]),
"deny Exec domain\n",
);
assert_eq!(unv(&rep, &pol, None), 1, "unfiltered: the inherited hole is a hole");
assert_eq!(unv(&rep, &pol, Some("dynamic")), 1, "--class dynamic must exclude nothing");
assert_eq!(unv(&rep, &pol, Some("*")), 1, "--class * must exclude nothing");
assert_eq!(unv(&rep, &pol, Some("dispatch")), 1, "inherited Unknown carries the callee's class");
assert_eq!(unv(&rep, &pol, Some("native")), 0, "no native reason anywhere in the reach");
assert_eq!(unv(&rep, &pol, Some("reflect")), 0, "no reflect reason anywhere in the reach");
assert_eq!(unv(&rep, &pol, Some("unresolved")), 0, "an inherited, CLASSIFIED hole is not `unresolved`");
}
#[test]
fn unverified_class_fails_closed_on_an_unnamed_direct_unknown() {
let (rep, pol) = unv_fixture(
"failclosed",
serde_json::json!([
{ "fn": "infra::mute", "inferred": ["Unknown"], "direct": ["Unknown"] },
{ "fn": "infra::murky", "inferred": ["Unknown"], "direct": ["Unknown"],
"unknownWhy": ["dispatch:Port"] },
{ "fn": "domain::both", "inferred": ["Unknown"],
"calls": ["infra::mute", "infra::murky"] },
]),
"deny Exec domain\n",
);
assert_eq!(unv(&rep, &pol, None), 1);
assert_eq!(unv(&rep, &pol, Some("dynamic")), 1, "--class dynamic must exclude nothing");
assert_eq!(unv(&rep, &pol, Some("unresolved")), 1, "the unnamed direct Unknown contributes `unresolved`");
assert_eq!(unv(&rep, &pol, Some("dispatch")), 1, "the named callee still contributes `dispatch`");
assert_eq!(unv(&rep, &pol, Some("native")), 0, "still discriminates");
}
#[test]
fn reason_class_matches_keeps_the_unclassifiable() {
use candor_classify::policy::reason_class_matches;
fn want<'a>(ts: &[&'a str]) -> BTreeSet<&'a str> {
ts.iter().copied().collect()
}
assert!(reason_class_matches(None, &want(&["unresolved"])));
assert!(!reason_class_matches(None, &want(&["dispatch"])));
assert!(reason_class_matches(Some(&BTreeSet::new()), &want(&["unresolved"])));
let cs: BTreeSet<String> = ["dispatch".to_string()].into_iter().collect();
assert!(reason_class_matches(Some(&cs), &want(&["dispatch", "native"])));
assert!(!reason_class_matches(Some(&cs), &want(&["unresolved"])));
}