facett-buildthing/tests/robot_galois.rs
//! **RED-when-broken robot proof for the bidirectional feature↔dependency pane.**
//!
//! Rendered headless through a REAL `egui_kittest` AccessKit harness, driven by
//! REAL clicks, and asserted on APPLIED OUTPUT: which crates are in the build,
//! which graph edges went dashed, which node ids exist. Never `is_ok()`, never
//! "a frame was drawn".
//!
//! The fixture is not hand-written. The `CrateAnalysis` values come out of
//! `nornir-feature-graph`'s real resolver run over real manifest text, so the
//! whole chain — manifest → `F` → minimal causes → pane → pixels — is under
//! test. Hand-writing the analyses would test the pane against invented facts
//! and prove nothing about the thing that was actually wrong.
//!
//! The shape modelled is the measured one. On `nornir-auth-policy`, 2026-08-01,
//! `cargo tree -p … -e no-dev --no-default-features`:
//!
//! | features | closure | reqwest |
//! |---|---|---|
//! | (none) | 84 | no |
//! | `online` | 84 | no |
//! | `auth-oidc` | 120 | no |
//! | `auth-oidc,online` | 161 | **yes** |
use std::cell::RefCell;
use std::collections::{BTreeMap, BTreeSet};
use std::rc::Rc;
use egui_kittest::kittest::{By, Queryable};
use egui_kittest::Harness;
use facett_buildthing::galois_pane::{Action, CrateClass, FeatureAxis, GaloisPane};
use nornir_feature_graph::{FeatureGalois, ResolveConfig, Universe};
const POLICY: &str = r#"
[package]
name = "auth-policy"
version = "0.1.0"
[features]
online = ["oidc?/online", "oauth2?/online"]
auth-oidc = ["dep:oidc"]
auth-oauth2 = ["dep:oauth2"]
auth-bearer = ["dep:bearer"]
[dependencies]
serde = "1"
oidc = { version = "0.1", optional = true }
oauth2 = { version = "0.1", optional = true }
bearer = { 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 }
"#;
const OAUTH2: &str = r#"
[package]
name = "oauth2"
version = "0.1.0"
[features]
online = ["dep:reqwest"]
[dependencies]
reqwest = { version = "0.12", optional = true }
"#;
/// Derive the axis with the REAL resolver. Nothing below is hand-asserted data.
fn axis() -> FeatureAxis {
let mut u = Universe::default();
u.add_manifest_str("edda", "auth-policy", POLICY).unwrap();
u.add_manifest_str("edda", "oidc", OIDC).unwrap();
u.add_manifest_str("edda", "oauth2", OAUTH2).unwrap();
for n in ["serde", "reqwest", "bearer"] {
u.add_manifest_str("r", n, &format!("[package]\nname = \"{n}\"\nversion = \"1.0.0\"\n"))
.unwrap();
}
let g = FeatureGalois::new(&u, "auth-policy", ResolveConfig::default());
let features: Vec<String> = g.features().iter().cloned().collect();
let everything = g.pulls(g.features());
let mut analyses = BTreeMap::new();
for krate in &everything.packages {
if krate == "auth-policy" {
continue;
}
analyses.insert(krate.clone(), g.analyse(krate));
}
FeatureAxis {
repo: "edda".into(),
root: "auth-policy".into(),
features,
analyses,
// Stand-in for Mímir's `feature_gate_facts` counts.
gated_items: [("online".to_string(), 12usize), ("auth-oidc".to_string(), 30)]
.into_iter()
.collect(),
sizes: BTreeMap::new(),
provenance: "derived by nornir-feature-graph from manifests (test fixture)".into(),
}
}
fn harness_for(pane: Rc<RefCell<GaloisPane>>) -> Harness<'static> {
let p = pane.clone();
let mut h = Harness::builder()
.with_size(egui::vec2(1100.0, 900.0))
.build_ui(move |ui| {
p.borrow_mut().ui(ui);
});
h.run();
h.run();
h
}
/// The fixture itself must have the shape the test relies on. If the resolver
/// ever stops finding the conjunction, every assertion below would pass
/// vacuously against a graph with no AND-group in it.
#[test]
fn the_fixture_really_contains_a_conjunction() {
let a = axis();
assert!(a.analyses.contains_key("reqwest"), "reqwest must be on the axis");
assert_eq!(
a.class_of("reqwest"),
CrateClass::Conjunction,
"reqwest must be a CONJUNCTION — analyses: {:?}",
a.analyses.get("reqwest")
);
assert_eq!(a.class_of("serde"), CrateClass::Unconditional, "serde is the ⊥ case");
assert_eq!(a.class_of("bearer"), CrateClass::Single, "bearer is a one-click removal");
let causes = &a.analyses["reqwest"].causes;
assert_eq!(causes.len(), 2, "two conjunctions: {causes:?}");
assert!(causes.iter().all(|c| c.len() == 2), "both are pairs: {causes:?}");
}
/// **(1) Render non-blank** and **(2) click a FEATURE off** — the first adjoint.
/// A real AccessKit click on the `online` chip must make `reqwest` leave the
/// build and its conjunction edges go dashed.
#[test]
fn robot_clicks_a_feature_off_and_the_dependency_leaves() {
let pane = Rc::new(RefCell::new(GaloisPane::new(axis())));
let mut h = harness_for(pane.clone());
// (1) the clickable features reached the pixels as AccessKit nodes.
for label in ["online", "auth-oidc", "auth-oauth2", "auth-bearer"] {
assert!(
h.query_all(By::new().label_contains(label)).count() >= 1,
"the feature chip `{label}` must render as an AccessKit node"
);
}
let before = pane.borrow().state_json();
assert!(
before["present"].as_array().unwrap().iter().any(|c| c == "reqwest"),
"precondition: reqwest starts IN the build — else the click below proves nothing"
);
let dashed_before = before["edges"]
.as_array()
.unwrap()
.iter()
.filter(|e| e["dashed"] == true)
.count();
assert_eq!(dashed_before, 0, "nothing is dashed before any click");
// (2) THE CLICK — a real AccessKit activation, not a method call.
h.get_by_label("online").click();
h.run();
h.run();
let after = pane.borrow().state_json();
assert_eq!(
after["last_action"]["kind"], "toggle-feature",
"the click routed to the feature adjoint: {:?}",
after["last_action"]
);
assert_eq!(after["last_action"]["now_enabled"], false);
// APPLIED OUTPUT: reqwest left.
let absent: Vec<&str> =
after["absent"].as_array().unwrap().iter().filter_map(|v| v.as_str()).collect();
assert!(
absent.contains(&"reqwest"),
"clicking `online` off must remove reqwest — absent={absent:?}"
);
// And the picture changed with it: the conjunction edges are now dashed.
let dashed_after = after["edges"]
.as_array()
.unwrap()
.iter()
.filter(|e| e["dashed"] == true)
.count();
assert!(
dashed_after > 0,
"the graph must SHOW the loss — no edge went dashed, so the picture still \
claims reqwest is pulled"
);
// The consequence, not just the toggle.
let cons = after["consequence"].as_str().unwrap();
assert!(
cons.contains("12 gated items"),
"the pane must report the gated items that stop compiling: {cons}"
);
assert_eq!(after["gated_lost"], 12);
// (3) click it back ON — the graph returns.
h.get_by_label("online").click();
h.run();
let back = pane.borrow().state_json();
assert!(
back["present"].as_array().unwrap().iter().any(|c| c == "reqwest"),
"clicking `online` back on must restore reqwest"
);
assert_eq!(back["last_action"]["now_enabled"], true);
}
/// **The other adjoint** — click the DEPENDENCY, and the features that pull it
/// turn off. Routed through `on_click`, the same path `draw_graph`'s
/// `clicked_node` takes.
#[test]
fn robot_clicks_a_dependency_off_and_the_features_go() {
let pane = Rc::new(RefCell::new(GaloisPane::new(axis())));
let mut h = harness_for(pane.clone());
// The crate node must exist in the model that gets painted.
let sj = pane.borrow().state_json();
let ids: Vec<&str> =
sj["node_ids"].as_array().unwrap().iter().filter_map(|v| v.as_str()).collect();
assert!(ids.contains(&"crate:reqwest"), "the reqwest node must be drawn: {ids:?}");
pane.borrow_mut().on_click("crate:reqwest");
h.run();
let after = pane.borrow().state_json();
assert_eq!(
after["last_action"]["kind"], "drop-crate",
"clicking a dependency must run the drop adjoint: {:?}",
after["last_action"]
);
let disabled: Vec<&str> = after["last_action"]["disabled"]
.as_array()
.unwrap()
.iter()
.filter_map(|v| v.as_str())
.collect();
// Both conjunctions share `online`, so ONE feature is the whole plan. A UI
// that turned off auth-oidc AND auth-oauth2 would be overstating the cost.
assert_eq!(disabled, vec!["online"], "the minimum hitting set is one feature");
assert!(
after["absent"].as_array().unwrap().iter().any(|c| c == "reqwest"),
"and it actually left the build"
);
assert_eq!(after["disabled"].as_array().unwrap().len(), 1);
// Click it again → it comes back. The connection runs both ways.
pane.borrow_mut().on_click("crate:reqwest");
h.run();
let back = pane.borrow().state_json();
assert_eq!(back["last_action"]["kind"], "restore-crate");
assert!(back["present"].as_array().unwrap().iter().any(|c| c == "reqwest"));
}
/// **The anti-lie assertion.** Clicking ONE half of an AND-group removes the
/// crate. A UI that said "not removable" here — which is what the old
/// `features_pulling(reqwest) == []` produced — would be repeating the measured
/// bug in pictures.
#[test]
fn breaking_one_conjunct_is_enough_and_the_graph_says_so() {
let pane = Rc::new(RefCell::new(GaloisPane::new(axis())));
let mut h = harness_for(pane.clone());
let sj = pane.borrow().state_json();
let conj: Vec<&str> = sj["conjunction_nodes"]
.as_array()
.unwrap()
.iter()
.filter_map(|v| v.as_str())
.collect();
assert_eq!(
conj.len(),
2,
"both AND-groups must be DRAWN as their own nodes — a bipartite graph without \
them cannot distinguish 'either' from 'both': {conj:?}"
);
// And badged, so it reads as a conjunction without hovering.
assert_eq!(
sj["badges"]["crate:reqwest"], "AND 2",
"reqwest must carry a conjunction badge: {:?}",
sj["badges"]
);
assert_eq!(sj["badges"]["crate:serde"], "ALWAYS", "the unconditional crate is badged ALWAYS");
assert_eq!(sj["classes"]["reqwest"], "conjunction");
assert_eq!(sj["classes"]["bearer"], "single");
let node = conj[0].to_string();
pane.borrow_mut().on_click(&node);
h.run();
let after = pane.borrow().state_json();
assert_eq!(
after["last_action"]["kind"], "break-conjunction",
"clicking the AND node must break it: {:?}",
after["last_action"]
);
let killed = after["last_action"]["disabled"].as_str().unwrap();
let conjuncts: Vec<&str> = after["last_action"]["conjuncts"]
.as_array()
.unwrap()
.iter()
.filter_map(|v| v.as_str())
.collect();
assert_eq!(conjuncts.len(), 2, "it is a pair");
assert!(conjuncts.contains(&killed), "the disabled feature is one of the pair");
assert_eq!(
after["disabled"].as_array().unwrap().len(),
1,
"exactly ONE feature was turned off — one conjunct is enough for an AND-group"
);
// Whether reqwest left depends on which conjunct: killing `online` kills
// both causes, killing `auth-oidc` leaves the oauth2 cause. Either way the
// AND-node itself must now read unsatisfied, which is the honest picture.
let unsat_edges = after["edges"]
.as_array()
.unwrap()
.iter()
.filter(|e| e["dashed"] == true)
.count();
assert!(unsat_edges > 0, "breaking a conjunct must dash its edges");
}
/// **The ⊥ case must refuse, out loud.** Clicking an unconditional dependency
/// changes nothing — and the pane says why instead of swallowing the click.
#[test]
fn an_unconditional_dependency_refuses_and_explains() {
let pane = Rc::new(RefCell::new(GaloisPane::new(axis())));
let mut h = harness_for(pane.clone());
pane.borrow_mut().on_click("crate:serde");
h.run();
let after = pane.borrow().state_json();
assert_eq!(
after["last_action"]["kind"], "refused",
"an unconditional crate must REFUSE: {:?}",
after["last_action"]
);
let reason = after["last_action"]["reason"].as_str().unwrap();
assert!(
reason.contains("unconditionally"),
"the refusal must say why, in words: {reason}"
);
assert!(
after["disabled"].as_array().unwrap().is_empty(),
"nothing was turned off"
);
assert!(
after["present"].as_array().unwrap().iter().any(|c| c == "serde"),
"and serde is still in the build"
);
// The refusal reaches the pixels, not just the JSON.
assert!(
h.query_all(By::new().label_contains("unconditionally")).count() >= 1,
"the refusal must RENDER — a swallowed click teaches the user nothing"
);
}
/// Toggling everything off and back on must return to exactly the starting
/// state. A pane that leaks disabled features would drift away from the truth
/// the resolver computed.
#[test]
fn the_pane_returns_to_where_it_started() {
let pane = Rc::new(RefCell::new(GaloisPane::new(axis())));
let mut h = harness_for(pane.clone());
let start = pane.borrow().state_json();
let feats: Vec<String> = pane.borrow().axis().features.clone();
for f in &feats {
pane.borrow_mut().toggle_feature(f);
}
h.run();
let all_off = pane.borrow().state_json();
assert_ne!(all_off["present"], start["present"], "turning everything off must change something");
// Only the ⊥ crates survive — that is the definition of ⊥, applied.
let present: BTreeSet<&str> =
all_off["present"].as_array().unwrap().iter().filter_map(|v| v.as_str()).collect();
let expected: BTreeSet<String> = pane
.borrow()
.axis()
.analyses
.iter()
.filter(|(_, a)| a.status == nornir_feature_graph::Reachability::Unconditional)
.map(|(k, _)| k.clone())
.collect();
let present_owned: BTreeSet<String> = present.iter().map(|s| s.to_string()).collect();
assert_eq!(present_owned, expected, "with every feature off, exactly the ⊥ set remains");
assert!(!expected.is_empty(), "the ⊥ set must not be empty, or this asserts nothing");
for f in &feats {
pane.borrow_mut().toggle_feature(f);
}
h.run();
let back = pane.borrow().state_json();
assert_eq!(back["present"], start["present"], "round trip must restore the build");
assert_eq!(back["edges"], start["edges"], "and the picture");
}
/// A click on something that is not on the axis must be REFUSED by name, never
/// silently ignored.
#[test]
fn an_unknown_target_is_named_not_swallowed() {
let mut pane = GaloisPane::new(axis());
assert!(pane.toggle_feature("no-such-feature").is_empty());
match pane.last_action() {
Some(Action::Refused { target, reason }) => {
assert_eq!(target, "no-such-feature");
assert!(reason.contains("not a declared feature"), "{reason}");
}
other => panic!("expected a named refusal, got {other:?}"),
}
assert!(pane.toggle_crate("no-such-crate").is_empty());
match pane.last_action() {
Some(Action::Refused { target, .. }) => assert_eq!(target, "no-such-crate"),
other => panic!("expected a named refusal, got {other:?}"),
}
}
/// **See the frame.** Renders the pane before and after a click and writes both
/// PNGs, so the change is something a human can look at rather than take on
/// trust. Skips (loudly) with no wgpu backend, like the other facett pixel legs.
///
/// This is the harness every other robot suite in the fleet was told to model on, so
/// it carries all FOUR legs of the STINKY-19 R0 pixel-proof pattern, in order:
///
/// 1. compare **raw RGBA**, never encoded PNG bytes (encoder state is not a picture);
/// 2. a **same-input determinism control** first, which measures the noise floor;
/// 3. a **percent** floor — a caption or a port number cannot reach it;
/// 4. an **extent** floor in both axes — a title bar or a status line cannot reach it.
///
/// Legs 1, 3 and 4 live in `facett_core::golden::moved`; leg 2 is
/// `facett_core::golden::stable`.
///
/// # Measured on oden, 2026-08-22 (1100×760 = 836 000 px, wgpu)
///
/// | state | changed pixels |
/// |---------------------------------------------------------|-----------------------|
/// | two renders of the SAME state (noise floor) | **0** — 0.0000 % |
/// | production frozen: `toggle_feature` made a no-op | **271** — 0.0324 % |
/// | intact click, bbox 884×483 | **29 535** — 3.53 % |
///
/// floor = 0.5 %: **15× above** the frozen state, **7× below** the intact one.
///
/// The frozen row is the finding, and it was measured on THIS file rather than argued.
/// The click was dead and 271 pixels still moved — the hover highlight under the
/// cursor — so `galois_before.png` and `galois_after.png` came out at different sizes
/// with different sha256. The assertion this test used to carry,
/// `assert_ne!(fs::read(&bp), fs::read(&ap))`, was therefore GREEN over a completely
/// dead click. That is why the shape had to change and not just the threshold.
#[test]
fn galois_pane_png_blobs_before_and_after_a_click() {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/snapshots");
std::fs::create_dir_all(&dir).unwrap();
let pane = Rc::new(RefCell::new(GaloisPane::new(axis())));
let p = pane.clone();
let mut h = Harness::builder()
.with_size(egui::vec2(1100.0, 760.0))
.build_ui(move |ui| {
p.borrow_mut().ui(ui);
});
h.run_steps(3);
let before = match h.render() {
Ok(i) => i,
Err(e) => {
eprintln!("[robot] no wgpu backend ({e}) — galois PNG blobs skipped");
return;
}
};
let bp = dir.join("galois_before.png");
before.save(&bp).expect("save galois_before.png");
// ── The DETERMINISM CONTROL, and it runs BEFORE the click on purpose. ──────────
//
// Render the pane a second time with NO input in between. Two renders of the same
// state must be byte-identical, and the number that comes out of this is the NOISE
// FLOOR the later before/after figure has to clear. Without it, a before/after
// pixel assertion — even a good one — cannot tell the interaction from the host:
// driver dithering, an unpinned clock or an easing animation would be credited to
// the click. `moved`'s floors below were chosen against THIS measurement, so the
// measurement has to actually be taken and not assumed.
let control = h.render().expect("a second render of the SAME state");
let (fw, fh) = (control.width(), control.height());
if let Err(why) = facett_core::golden::stable(&before, &control) {
panic!("[robot] galois pane determinism control FAILED\n {why}");
}
eprintln!(
"[robot] noise floor: 0 of {} px (0.0000 % of frame) — two renders of the same state \
are byte-identical",
u64::from(fw) * u64::from(fh)
);
// The click that matters: `online` off → reqwest leaves.
h.get_by_label("online").click();
h.run_steps(3);
let after = h.render().expect("second render");
let ap = dir.join("galois_after.png");
after.save(&ap).expect("save galois_after.png");
let (b, a) = (
std::fs::metadata(&bp).unwrap().len(),
std::fs::metadata(&ap).unwrap().len(),
);
assert!(b > 1000 && a > 1000, "both frames are real ({b} / {a} bytes)");
// The PIXELS must differ, and differ ENOUGH to be the pane redrawing.
//
// This assertion used to be `assert_ne!(fs::read(&bp), fs::read(&ap))` — a whole-file
// comparison of the ENCODED PNGs. That is the shape every other robot suite copied
// from this test, and it cannot fail for the reason it claims: three suites were
// independently measured passing it on a window title, an ephemeral port number and
// a text legend. Encoded bytes also carry metadata and encoder state, so "the bytes
// differ" is not even "the picture differs".
//
// `facett_core::golden::moved` is the one writer for this question. It compares
// DECODED pixels and names the floor a real redraw has to clear: a share of the
// frame, plus a bounding box with extent in BOTH axes so a thin strip (a title, a
// status line) cannot satisfy it. Its own tests assert it rejects all three of the
// observed false passes.
let d = facett_core::golden::moved(&before, &after, facett_core::golden::MinChange::PANE)
.expect("the `online` click must visibly redraw the galois pane");
eprintln!(
"[robot] pane moved: {} px ({:.2} % of frame), bbox {:?}, max channel delta {}",
d.pixels,
d.percent(),
d.bbox,
d.max_channel_delta
);
// And the state really did move, so the pixel difference is the right one.
assert!(pane.borrow().state_json()["absent"]
.as_array()
.unwrap()
.iter()
.any(|c| c == "reqwest"));
eprintln!("[robot] wrote {} ({b} B) and {} ({a} B)", bp.display(), ap.display());
}