crates/nornir-feature-graph/tests/and_group_is_the_red_test.rs
//! **The measured red test.**
//!
//! `nornir-auth-policy` declares `online = ["nornir-auth-oidc?/online", …]`. The
//! `?` is a WEAK dependency: it turns the sub-crate's feature on only if the
//! sub-crate is already there. So `reqwest` arrives only under the conjunction
//! `auth-oidc ∧ online`, and under NEITHER feature alone.
//!
//! Measured 2026-08-01 with `cargo tree -p nornir-auth-policy -e no-dev
//! --no-default-features`:
//!
//! | features | closure | reqwest |
//! |---|---|---|
//! | (none) | 72 | no |
//! | `online` | 72 | no |
//! | `auth-oidc` | 101 | no |
//! | `auth-oidc,online` | **140** | **yes** |
//!
//! 39 of 140 crates (27.9%) exist only under the conjunction.
//!
//! The old hand-maintained model answered `features_pulling("reqwest") == []`
//! and therefore `drop_crate("reqwest").removable == false` — a false negative
//! on the exact crate holger's airgap tier exists to remove. These tests pin
//! the corrected answer, on a self-contained fixture that reproduces the shape
//! exactly (so they run without the constellation on disk).
use nornir_feature_graph::{FeatureGalois, Reachability, ResolveConfig, Universe};
use std::collections::BTreeSet;
const POLICY: &str = r#"
[package]
name = "auth-policy"
version = "0.1.0"
[features]
default = []
online = ["auth-oidc-crate?/online", "auth-oauth2-crate?/online"]
auth-bearer = ["dep:auth-bearer-crate"]
auth-oidc = ["dep:auth-oidc-crate"]
auth-oauth2 = ["dep:auth-oauth2-crate"]
auth-mtls = ["dep:auth-mtls-crate"]
[dependencies]
serde = "1"
auth-bearer-crate = { version = "0.1", optional = true }
auth-oidc-crate = { version = "0.1", optional = true }
auth-oauth2-crate = { version = "0.1", optional = true }
auth-mtls-crate = { version = "0.1", optional = true }
"#;
const OIDC: &str = r#"
[package]
name = "auth-oidc-crate"
version = "0.1.0"
[features]
default = []
online = ["dep:reqwest"]
[dependencies]
serde = "1"
reqwest = { version = "0.12", optional = true }
"#;
const OAUTH2: &str = r#"
[package]
name = "auth-oauth2-crate"
version = "0.1.0"
[features]
default = []
online = ["dep:reqwest"]
[dependencies]
reqwest = { version = "0.12", optional = true }
"#;
const LEAF: &str = r#"
[package]
name = "reqwest"
version = "0.12.0"
[dependencies]
hyper = "1"
"#;
fn world() -> Universe {
let mut u = Universe::default();
u.add_manifest_str("edda", "auth-policy", POLICY).unwrap();
u.add_manifest_str("edda", "auth-oidc-crate", OIDC).unwrap();
u.add_manifest_str("edda", "auth-oauth2-crate", OAUTH2).unwrap();
u.add_manifest_str("registry", "reqwest", LEAF).unwrap();
for n in ["serde", "hyper", "auth-bearer-crate", "auth-mtls-crate"] {
u.add_manifest_str("registry", n, &format!("[package]\nname = \"{n}\"\nversion = \"1.0.0\"\n"))
.unwrap();
}
u
}
fn set(items: &[&str]) -> BTreeSet<String> {
items.iter().map(|s| s.to_string()).collect()
}
fn galois(u: &Universe) -> FeatureGalois<'_> {
FeatureGalois::new(u, "auth-policy", ResolveConfig::default())
}
/// The forward map must reproduce the MEASURED table above: neither feature
/// alone brings reqwest, both together do. If this ever passes trivially the
/// fixture has stopped modelling a weak dep.
#[test]
fn the_conjunction_is_real_and_neither_half_is_enough() {
let u = world();
let g = galois(&u);
assert!(!g.pulls(&set(&[])).contains("reqwest"), "reqwest must be absent with no features");
assert!(
!g.pulls(&set(&["online"])).contains("reqwest"),
"`online` ALONE must not pull reqwest — the weak `?` means the sub-crate is not there \
to have its feature turned on"
);
assert!(
!g.pulls(&set(&["auth-oidc"])).contains("reqwest"),
"`auth-oidc` ALONE must not pull reqwest — the sub-crate is there but its `online` \
feature is off"
);
assert!(
g.pulls(&set(&["auth-oidc", "online"])).contains("reqwest"),
"the CONJUNCTION must pull reqwest"
);
}
/// **The headline correction.** The old answer was `[]`.
#[test]
fn features_pulling_reqwest_is_not_empty() {
let u = world();
let g = galois(&u);
let (causes, complete) = g.features_pulling("reqwest");
assert!(
!causes.is_empty(),
"REGRESSION to the measured bug: features_pulling(\"reqwest\") came back EMPTY. \
That is the naive single-feature preimage, and it reports a removable crate as \
unremovable."
);
assert!(complete, "the cause enumeration must be provably complete for a graph this small");
// Assert the APPLIED content, not just non-emptiness: every cause must be a
// two-element conjunction, and both must be present.
let expected: Vec<BTreeSet<String>> =
vec![set(&["auth-oidc", "online"]), set(&["auth-oauth2", "online"])];
let mut got = causes.clone();
got.sort();
let mut want = expected.clone();
want.sort();
assert_eq!(got, want, "the two AND-groups, exactly");
for c in &causes {
assert_eq!(c.len(), 2, "every cause here is a conjunction: {c:?}");
}
}
/// The lower adjoint must say removable, and must name a set that ACTUALLY
/// works when re-resolved — not one that merely looks right on paper.
#[test]
fn drop_crate_reqwest_is_removable_and_verified() {
let u = world();
let g = galois(&u);
let plan = g.drop_crate("reqwest");
assert!(
plan.removable,
"REGRESSION to the measured bug: reqwest reported NOT removable. note={}",
plan.note
);
assert!(plan.verified, "the plan must be confirmed by re-resolving, not asserted: {plan:?}");
// Both causes contain `online`, so the minimum hitting set is that one
// feature — turning off `online` is enough, and turning off `auth-oidc`
// alone is NOT (oauth2 still reaches reqwest).
assert_eq!(
plan.features_to_disable,
set(&["online"]),
"one feature hits both conjunctions"
);
// And prove it applies: re-resolve with everything except the plan.
let all = g.features().clone();
let remaining: BTreeSet<String> =
all.difference(&plan.features_to_disable).cloned().collect();
assert!(
!g.pulls(&remaining).contains("reqwest"),
"the plan did not actually remove reqwest"
);
// The negative control: the OTHER obvious guess does not work.
let naive: BTreeSet<String> = all.difference(&set(&["auth-oidc"])).cloned().collect();
assert!(
g.pulls(&naive).contains("reqwest"),
"turning off auth-oidc alone must NOT remove reqwest — auth-oauth2 still pulls it. \
If this passes, the fixture lost its second cause and the test above is weaker \
than it looks."
);
}
/// The three cases a UI must render differently. A graph that draws them the
/// same lies to the user about what a click will do.
#[test]
fn the_three_removability_classes_are_distinguishable() {
let u = world();
let g = galois(&u);
// ⊥ — unconditional. No click removes it.
let serde = g.analyse("serde");
assert_eq!(serde.status, Reachability::Unconditional);
assert!(!serde.drop.removable);
assert!(serde.causes.is_empty(), "an unconditional crate has no cause to show");
// Exactly one feature.
let bearer = g.analyse("auth-bearer-crate");
assert_eq!(bearer.status, Reachability::Conditional);
assert_eq!(bearer.single_cause(), Some("auth-bearer"));
assert!(bearer.and_groups().is_empty());
// A conjunction.
let reqwest = g.analyse("reqwest");
assert_eq!(reqwest.status, Reachability::Conditional);
assert_eq!(reqwest.single_cause(), None, "reqwest is NOT a one-click removal");
assert_eq!(reqwest.and_groups().len(), 2, "both conjunctions must be renderable");
// Never reachable — nothing to draw, and it must not be confused with ⊥.
let ghost = g.analyse("no-such-crate");
assert_eq!(ghost.status, Reachability::Never);
assert!(!ghost.drop.removable);
assert_ne!(ghost.status, serde.status, "unreachable and unconditional are different facts");
}
/// The two directions of the connection, from ONE `F`. Clicking a feature off
/// must lose exactly the crates whose every cause it broke.
#[test]
fn both_adjoints_agree_because_they_are_the_same_map() {
let u = world();
let g = galois(&u);
// Direction 1: click `online` off → what leaves?
let lost = g.crates_lost_by_disabling("online");
assert!(lost.contains("reqwest"), "turning `online` off must lose reqwest; lost={lost:?}");
// Direction 2: for each crate lost, the drop plan must have named `online`
// as sufficient — the two adjoints read off the same causes.
for c in &lost {
let plan = g.drop_crate(c);
assert!(plan.removable, "{c} was lost by a click but reports unremovable: {}", plan.note);
assert!(plan.verified, "{c}: unverified plan");
}
// The naive single-feature view DISAGREES, and that disagreement is the
// AND-group. Pinning it stops anyone "simplifying" back to the old model.
let alone = g.crates_pulled_alone("online");
assert!(
!alone.contains("reqwest"),
"`online` on its own pulls nothing — that is why the naive model returned []"
);
assert!(
lost.contains("reqwest") && !alone.contains("reqwest"),
"the two readings MUST differ here; if they ever agree the weak dep stopped being \
modelled and every AND-group silently became invisible again"
);
}