Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

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"
    );
}