crates/nornir-feature-graph/tests/warehouse_roundtrip.rs
//! **The warehouse round-trip, and the deleted-edge red proof.**
//!
//! Build Thing does not re-walk the disk: it reads `feature_dep_edges` out of
//! Mímir and reconstitutes the world. So the rows must carry enough to rebuild
//! the same answers — and a MISSING row must change the answer, loudly, rather
//! than quietly degrading into a smaller graph that still looks plausible.
//!
//! The second test is the one the brief asked for: delete one edge from the
//! derived table and watch the check go red.
use nornir_feature_graph::{
facts::EnableKind, universe_from_rows, FeatureDepEdgeRow, FeatureGalois, ResolveConfig,
Universe,
};
use std::collections::BTreeSet;
const POLICY: &str = r#"
[package]
name = "auth-policy"
version = "0.1.0"
[features]
online = ["oidc?/online"]
auth-oidc = ["dep:oidc"]
[dependencies]
serde = "1"
oidc = { version = "0.1", optional = true }
"#;
const OIDC: &str = r#"
[package]
name = "oidc"
version = "0.1.0"
[features]
online = ["dep:reqwest"]
[dependencies]
reqwest = { version = "0.12", optional = true }
"#;
fn world() -> Universe {
let mut u = Universe::default();
u.add_manifest_str("edda", "auth-policy", POLICY).unwrap();
u.add_manifest_str("edda", "oidc", OIDC).unwrap();
for n in ["serde", "reqwest"] {
u.add_manifest_str("r", n, &format!("[package]\nname = \"{n}\"\nversion = \"1.0.0\"\n"))
.unwrap();
}
u
}
fn rows(u: &Universe) -> Vec<FeatureDepEdgeRow> {
let mut r: Vec<_> = u.packages().flat_map(|p| p.edges()).collect();
r.sort();
r.dedup();
r
}
fn answer(u: &Universe) -> (Vec<BTreeSet<String>>, bool) {
let g = FeatureGalois::new(u, "auth-policy", ResolveConfig::default());
g.features_pulling("reqwest")
}
/// The rows must reconstitute the world. Asserted on the ANSWER — the actual
/// cause sets — not on a row count, because equal row counts prove nothing
/// about whether the semantics survived the trip.
#[test]
fn rows_reconstitute_the_same_answers() {
let disk = world();
let table = rows(&disk);
assert!(!table.is_empty(), "the fixture produced no rows at all");
// The weak edge MUST be in the table — it is the only thing that makes the
// AND-group representable, and a table without it is a table that cannot
// answer the question this crate exists for.
assert!(
table.iter().any(|r| r.kind == EnableKind::WeakDepFeature && r.enables == "oidc/online"),
"no weak-dep-feature row emitted; table = {table:#?}"
);
// And the unconditional ⊥ sentinel.
assert!(
table.iter().any(|r| r.feature.is_empty() && r.enables == "serde"),
"no `feature = \"\"` unconditional row for serde — the ⊥ case is unrepresentable"
);
let from_wh = universe_from_rows(&table).expect("non-empty table must build a universe");
let (disk_causes, disk_complete) = answer(&disk);
let (wh_causes, wh_complete) = answer(&from_wh);
assert_eq!(
disk_causes,
vec![["auth-oidc".to_string(), "online".to_string()].into_iter().collect::<BTreeSet<_>>()],
"the on-disk answer is the conjunction"
);
assert_eq!(wh_causes, disk_causes, "the warehouse round-trip changed the answer");
assert_eq!(wh_complete, disk_complete);
}
/// **The red proof.** Delete ONE edge from the derived table — the weak
/// `oidc?/online` row — and the answer must change. If it does not, the table
/// is carrying redundant information and the check is watching nothing.
#[test]
fn deleting_one_edge_changes_the_answer() {
let disk = world();
let full = rows(&disk);
let pruned: Vec<FeatureDepEdgeRow> = full
.iter()
.filter(|r| !(r.kind == EnableKind::WeakDepFeature && r.enables == "oidc/online"))
.cloned()
.collect();
assert_eq!(pruned.len(), full.len() - 1, "exactly one row must have been removed");
let (before, _) = answer(&universe_from_rows(&full).unwrap());
let (after, _) = answer(&universe_from_rows(&pruned).unwrap());
assert!(!before.is_empty(), "precondition: the intact table finds the cause");
assert!(
after.is_empty(),
"DELETING THE WEAK EDGE DID NOT CHANGE THE ANSWER — the check is blind. \
before={before:?} after={after:?}"
);
assert_ne!(before, after, "one deleted row must move the answer");
}
/// A universe built from ZERO rows must be an ERROR, never an empty universe
/// that confidently answers "nothing depends on this".
///
/// This exact confusion — querying the wrong warehouse and reading 0 rows as a
/// fact — cost an hour on 2026-07-31.
#[test]
fn an_empty_table_is_loud_not_empty() {
let err = universe_from_rows(&[]).expect_err("zero rows MUST be an error");
let msg = err.to_string();
assert!(
msg.contains("ZERO rows") && msg.contains("never populated"),
"the error must say empty-vs-absent in words: {msg}"
);
// The negative control: one row is enough to succeed, so the guard is
// reacting to emptiness and not simply always failing.
let one = vec![FeatureDepEdgeRow {
repo: "r".into(),
crate_name: "a".into(),
feature: "f".into(),
enables: "b".into(),
kind: EnableKind::Dep,
optional: true,
target_cfg: None,
dep_kind: nornir_feature_graph::DepKind::Normal,
}];
assert!(universe_from_rows(&one).is_ok(), "one row must succeed — else the guard is not about emptiness");
}