//! **The UI tier ladder, measured.**
//!
//! `holger-server-ui` ships four tiers from one source tree — `gui-embedded` (bin
//! `holger-server-ui-embedded`), `gui` (bin `holger-server-ui`), `gui-mini` (bin
//! `holger-server-ui-mini`) and `gui-mikro` (bin `holger-server-ui-mikro`; the four
//! bins, and the package, were `holger-ui*` until 2026-08-01, when they were
//! renamed to say WHICH component they drive) — and each is supposed
//! to be strictly leaner than the one above it.
//!
//! Only the bottom three are SHIPPED clients; `gui-embedded` is the opt-in build
//! that may open state in its own process, and it sits at the top of the ladder
//! precisely because it is the fattest thing this tree can produce. Whether each
//! tier is THIN is a different question, asked by `tests/thin_client.rs`.
//!
//! ## Why this file exists
//!
//! It was not. `server/ui/Cargo.toml` declared `mini = ["gui"]`, and **a Cargo
//! feature cannot subtract a dependency** — `mini` enabled `gui` and removed
//! nothing, so the "mini" binary linked the identical closure as the full one and
//! then added tab-filtering code on top. Measured 2026-07-22 in debug, under the
//! bin names of the day:
//!
//! ```text
//! holger-ui 1 048 139 472 B
//! holger-ui-mikro 1 050 137 592 B ← the LEAN tier, 2 MB BIGGER
//! ```
//!
//! Every declaration in the repo said otherwise. `.nornir/build-variants.toml`
//! described a lean read-only client; `app.rs` cut eleven tabs down to six; the
//! variant's own comment called it a tier. All true, all about the SURFACE, and
//! none of it about the link — because nothing ever compared the two artifacts.
//!
//! ## What it asserts, and why in this order
//!
//! * [`the_tier_ladder_is_strictly_leaner_by_crate_count`] — always runs, costs a
//! few seconds, and is the guard that would have caught `mini = ["gui"]` the day
//! it was written: under that declaration the three closures are IDENTICAL, and
//! "identical" fails a *strictly* leaner assertion. It also asserts that the two
//! read-only tiers link no outbound HTTP client and no skade.
//! * [`the_tier_ladder_is_strictly_leaner_in_bytes`] — `#[ignore]`d because it
//! release-builds four egui applications. This is the half that measures the
//! artifact rather than the resolve: bytes, plus `nm` symbol probes for
//! `reqwest` / `ureq` / `skade`.
//!
//! Run the expensive half with:
//!
//! ```text
//! cargo test -p holger-server-ui --test tier_ladder -- --ignored --nocapture
//! ```
//!
//! Measured 2026-07-23, release:
//!
//! ```text
//! gui-embedded holger-server-ui-embedded 64 732 640 B 608 crates
//! gui holger-server-ui 31 603 160 B 534 crates
//! gui-mini holger-server-ui-mini 20 685 888 B 493 crates
//! gui-mikro holger-server-ui-mikro 20 488 072 B 492 crates
//! ```
//!
//! **Neither test reads `Cargo.toml`.** A declaration is not evidence; that is the
//! whole lesson of the bug this file guards.
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use std::process::Command;
/// The tiers, fattest first. The ladder must hold pairwise down this list.
///
/// `(tier feature, bin name)` — the bin is what `[[bin]] required-features` pairs
/// with the feature in `server/ui/Cargo.toml`, and what the byte half measures.
const LADDER: [(&str, &str); 4] = [
("gui-embedded", "holger-server-ui-embedded"),
("gui", "holger-server-ui"),
("gui-mini", "holger-server-ui-mini"),
("gui-mikro", "holger-server-ui-mikro"),
];
/// Crates that must not appear in a read-only tier's closure.
///
/// `reqwest` / `ureq` are the outbound HTTP clients; `openssl` / `native-tls` ride
/// in with them. `skade` is the Iceberg warehouse the Tools advisory scan reaches
/// through `modgunn/warehouse`, and it is the single heaviest thing the full tier
/// pulls that a read-only client has no use for.
///
/// `hyper` / `tonic` / `h2` are deliberately ABSENT from this list: they are the
/// gRPC channel every tier legitimately links to talk to the registry at all. A
/// check that failed a client for being able to reach its own server would be
/// switched off within a week.
const FORBIDDEN_IN_READ_ONLY: [&str; 5] = ["reqwest", "ureq", "skade", "openssl", "native-tls"];
/// The tiers the forbidden list applies to.
const READ_ONLY_TIERS: [&str; 2] = ["gui-mini", "gui-mikro"];
fn manifest_path() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml")
}
mod common;
use common::release_dir;
/// The RESOLVED dependency closure of one tier, as a set of crate names.
///
/// `cargo tree -e no-dev` is the resolve the build actually uses, so this measures
/// what will be linked rather than what the manifest says. Dev-dependencies are
/// excluded on purpose: they are not in the shipped artifact, and holger-server-ui's
/// dev-deps deliberately pull the full `server-lib` so the headless tests can run.
fn closure(tier: &str) -> BTreeSet<String> {
let out = Command::new(env!("CARGO"))
.args([
"tree",
"--manifest-path",
manifest_path().to_str().unwrap(),
"-p",
"holger-server-ui",
"--no-default-features",
"--features",
tier,
"-e",
"no-dev",
"--prefix",
"none",
])
.output()
.unwrap_or_else(|e| panic!("`cargo tree` for '{tier}' failed to start: {e}"));
assert!(
out.status.success(),
"`cargo tree` for '{tier}' failed:\n{}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout)
.lines()
.filter_map(|l| l.split_whitespace().next())
.filter(|c| !c.is_empty() && *c != "(*)")
.map(|c| c.to_string())
.collect()
}
/// **RED-when-broken: each tier's dependency closure is strictly smaller than the
/// tier above it, and it is a strict SUBSET — not merely a different set of the
/// same size.**
///
/// Subset, not just cardinality: a "lean" tier that dropped ten crates and added
/// nine would pass a count check while linking something the tier above it does
/// not, which is not a rung on a ladder — it is a fork.
#[test]
fn the_tier_ladder_is_strictly_leaner_by_crate_count() {
let measured: Vec<(&str, BTreeSet<String>)> =
LADDER.iter().map(|(tier, _)| (*tier, closure(tier))).collect();
for (tier, set) in &measured {
println!("{tier:>10}: {} crates", set.len());
}
for pair in measured.windows(2) {
let (fat, fat_set) = &pair[0];
let (lean, lean_set) = &pair[1];
let added: Vec<&String> = lean_set.difference(fat_set).collect();
assert!(
added.is_empty(),
"'{lean}' must be a SUBSET of '{fat}', but it links crates '{fat}' does not: {added:?}\n\
A tier that adds a dependency to the tier above it is a fork, not a rung."
);
let dropped: Vec<&String> = fat_set.difference(lean_set).collect();
assert!(
!dropped.is_empty(),
"'{lean}' links the SAME {} crates as '{fat}' — it is not leaner, it only \
looks leaner.\n\
This is exactly the `mini = [\"gui\"]` bug: a Cargo feature cannot subtract a \
dependency, so a tier that merely ADDS to the tier above it removes nothing. \
Make the heavy deps optional and have each tier select what it needs.",
fat_set.len()
);
println!("{fat} → {lean}: {} crates dropped", dropped.len());
}
// The read-only tiers link no general-purpose HTTP client and no skade. This is
// the claim that pairs them with the airgapped `mini` SERVER, and it is checked
// against the resolve rather than trusted.
for (tier, set) in &measured {
if !READ_ONLY_TIERS.contains(tier) {
continue;
}
for bad in FORBIDDEN_IN_READ_ONLY {
assert!(
!set.contains(bad),
"'{tier}' is a read-only client tier and must not link '{bad}'"
);
}
}
}
/// **RED-when-broken: each tier's release ARTIFACT is strictly smaller than the
/// tier above it, and the read-only tiers carry no `reqwest` / `ureq` / `skade`
/// symbols.**
///
/// `#[ignore]` because it release-builds three egui applications. The crate-count
/// test above is the everyday guard; this one is the proof, and it is the only
/// measurement that would have caught the original bug in the units the bug was
/// reported in — bytes on disk.
///
/// Note on the `skade` probe: a release binary of ANY tier reports two `skade`
/// hits from `facett_core::look::Theme::skade_vinter`, which is a THEME NAME, not
/// the crate. The threshold below is set accordingly, and the crate-level check in
/// the test above is the one that is exact.
#[test]
#[ignore = "release-builds four egui binaries; run with -- --ignored"]
fn the_tier_ladder_is_strictly_leaner_in_bytes() {
let release = release_dir();
let mut sizes: Vec<(&str, u64)> = Vec::new();
for (tier, bin) in LADDER {
let status = Command::new(env!("CARGO"))
.args([
"build",
"--release",
"--manifest-path",
manifest_path().to_str().unwrap(),
"-p",
"holger-server-ui",
"--no-default-features",
"--features",
tier,
"--bin",
bin,
])
.status()
.unwrap_or_else(|e| panic!("building '{bin}' failed to start: {e}"));
// `cargo build --all-targets` SILENTLY SKIPS a `required-features` bin. Each
// one is therefore named explicitly here — that omission is how five holger
// binaries went unbuilt for months.
assert!(status.success(), "building '{bin}' (--features {tier}) failed");
let path = release.join(bin);
let bytes = std::fs::metadata(&path)
.unwrap_or_else(|e| panic!("no artifact at {}: {e}", path.display()))
.len();
let probe = |sym: &str| -> usize {
let out = Command::new("nm").arg("-C").arg(&path).output();
match out {
Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout)
.lines()
.filter(|l| l.contains(sym))
.count(),
// No `nm` (or a stripped binary): the byte ladder still stands, and
// the crate-level check in the other test is the exact one.
_ => 0,
}
};
let (reqwest, ureq, skade) = (probe("reqwest"), probe("ureq"), probe("skade"));
println!("{tier:>10} {bin:<16} {bytes:>12} B reqwest={reqwest} ureq={ureq} skade={skade}");
if READ_ONLY_TIERS.contains(&tier) {
assert_eq!(reqwest, 0, "'{tier}' must carry no reqwest symbols");
assert_eq!(ureq, 0, "'{tier}' must carry no ureq symbols");
assert!(
skade <= 2,
"'{tier}' carries {skade} skade symbols — more than the two \
`Theme::skade_vinter` hits every tier has, so the skade CRATE is linked"
);
}
sizes.push((tier, bytes));
}
for pair in sizes.windows(2) {
let (fat, fat_bytes) = pair[0];
let (lean, lean_bytes) = pair[1];
assert!(
lean_bytes < fat_bytes,
"'{lean}' is {lean_bytes} B and '{fat}' is {fat_bytes} B — the lean tier is \
NOT smaller.\n\
That is the exact shape of the bug this file exists for: on 2026-07-22 the \
'mini' UI measured 2 MB LARGER than the full one, because `mini = [\"gui\"]` \
added tab-filtering code and removed no dependency."
);
}
}