Gunnar

nordisk/holger-egui

tests/thin_client.rs

//! **Every shipped holger UI tier is THIN, and this is where that is checked.**
//!
//! The constellation rule, in Rickard's words: *"se till att alla UI alltid är
//! default thin byggda och reflektera detta mot build thing!"* — every client
//! defaults to thin and talks to a running server.
//!
//! ## Why it matters here specifically
//!
//! A UI that embeds its own server shows **its own in-process world** instead of
//! the server's, with nothing on screen telling the operator which one they are
//! looking at — and it becomes a **second writer against the warehouse lock**.
//! ("Fat is the default" governs the *server/forge*, the thing that owns state. A
//! client is the other case.)
//!
//! The UI was in violation on three counts, all measured on 2026-07-23 before
//! the split, under the bin name of the day — `nm -C target/release/holger-ui`:
//!
//! | source | what it embedded | symbols |
//! |---|---|---|
//! | `demo` (world-connector + znippy-rust-repo) | `LocalHolger` over a znippy store it fills itself | `LocalHolger` 34 |
//! | `warehouse-scan` (`modgunn/warehouse` → skade) | an Iceberg catalog opened from `$HOLGER_WAREHOUSE` | `iceberg` 5642, `parquet` 2393 |
//! | `bench` (bench-scenarios) | a real holger HTTP gateway, stood up to be benchmarked | `tonic::transport::server` 5, `FastRoutes` 19 |
//!
//! All three moved to the opt-in `gui-embedded` tier (bin `holger-server-ui-embedded`),
//! mirroring dwarves' `--embedded`: **the zero-config path is thin, and the fat one
//! is something you ask for by name.** The full tier went from 64 733 488 B to
//! 31 603 160 B in the process — a client should not be half a server.
//!
//! ## The audit is SHARED, and no longer re-implemented here
//!
//! This file used to carry its own copy of `dwarves::build_thing::LinkageAudit` and
//! its own forbidden-symbol list, with a comment recording that as a compromise:
//! `dwarves` is the whole CI/CD forge (~127 dependencies), so dev-depending on it to
//! reach one struct would have made `cargo test -p holger-server-ui` build the forge. That
//! twin is now gone. [`LinkageAudit`] + [`THIN_FORBIDDEN_SYMBOLS`] live in
//! `nornir-build-thing::linkage` — the dep-light edda leaf every build thing already
//! depends on, and the crate that already owns the `linkage = "thin" | "fat"` schema
//! being checked. `cargo tree -p holger-server-ui --all-features | grep -c dwarves` → 0.
//!
//! holger keeps exactly what is holger's: its own two server entry points,
//! `FastRoutes` and `LocalHolger`, passed as extra families to
//! [`LinkageAudit::of_families`]. Per-app names do not belong in a constellation-wide
//! constant.
//!
//! ## What is NOT forbidden, and why that matters more than what is
//!
//! The shared list's design point, inherited verbatim: it targets **server entry
//! points and the storage engine**, never "any symbol mentioning holger". A thin
//! holger UI legitimately carries hundreds of `holger_server_lib` symbols —
//! `RemoteHolger`, the generated gRPC client stubs and the shared view types all live
//! in that crate — and 481 of them in `holger-server-ui-mini` are *correct*. So is `reqwest`
//! in the full tier: signing in over OIDC and migrating from a remote Nexus are
//! client work. A guard that failed those would be wrong, and would be switched off
//! within a week.
//!
//! Two further rules, now enforced by the shared type rather than restated here:
//!
//! * **An unmeasured audit never passes.** A missing binary or an unreadable `nm` is
//!   [`LinkageVerdict::NotMeasured`] — a failure, not a silent green tick.
//! * **A fat variant is never failed for being fat, and never passes for being
//!   empty.** `gui-embedded` is expected to carry every family below, and the shared
//!   audit fails it if it stops ([`LinkageVerdict::EmbeddedButEmpty`]) — because at
//!   that moment the thin check above it has silently become vacuous. holger asserts
//!   something *stricter* than the shared minimum: not merely ≥1 family present but
//!   ≥3, i.e. all three of the panes that were split out (demo → `LocalHolger`,
//!   warehouse-scan → `iceberg`/`parquet`, bench → the gateway).

use std::path::{Path, PathBuf};

use nornir_build_thing::linkage::{
    LinkageAudit, LinkageVerdict, ServiceShape, THIN_FORBIDDEN_SYMBOLS,
};

/// The shipped CLIENT tiers. **Every one must be thin** — that is the rule.
const THIN_TIERS: [(&str, &str); 3] =
    [("gui", "holger-server-ui"), ("gui-mini", "holger-server-ui-mini"), ("gui-mikro", "holger-server-ui-mikro")];

/// The one tier that is allowed to embed, and must therefore be asked for by name.
const EMBEDDED_TIER: (&str, &str) = ("gui-embedded", "holger-server-ui-embedded");

/// **holger's OWN server entry points**, on top of the shared
/// [`THIN_FORBIDDEN_SYMBOLS`] (which already names `tonic::transport::server` and the
/// `iceberg` / `parquet` / `datafusion` storage engine).
///
/// `holger_server_lib` as a whole is deliberately absent: a thin client links that
/// crate for `RemoteHolger` and the gRPC stubs, and failing it for that would fail
/// every client there is. What is named here is what only a SERVER needs — the axum
/// route table and the in-process core handle.
const HOLGER_SERVER_SYMBOLS: [&str; 2] = ["FastRoutes", "LocalHolger"];

/// The shared families plus holger's two.
fn families() -> Vec<&'static str> {
    THIN_FORBIDDEN_SYMBOLS.iter().copied().chain(HOLGER_SERVER_SYMBOLS).collect()
}

fn manifest_path() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml")
}

mod common;
use common::release_dir;

/// Build one tier's binary and audit its linkage against the declared shape.
///
/// Returns `None` only when the artifact is ABSENT after a successful build — which
/// should not happen, and the caller panics on it. An artifact that exists but cannot
/// be read comes back as an audit whose verdict is [`LinkageVerdict::NotMeasured`],
/// which never passes: an unchecked guarantee is not a guarantee.
fn audit(tier: &str, bin: &str, declared: ServiceShape) -> Option<LinkageAudit> {
    let status = std::process::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, so each
    // one is named explicitly. That omission is how five holger binaries went
    // unbuilt for months.
    assert!(status.success(), "building '{bin}' (--features {tier}) failed");

    LinkageAudit::of_families(tier, declared, &release_dir().join(bin), &families())
}

/// **RED-when-broken: every shipped UI tier links no server entry point and no
/// storage engine — and the one tier that does is the one you have to name.**
///
/// `#[ignore]` because it release-builds four egui applications. Run it with:
///
/// ```text
/// cargo test -p holger-server-ui --test thin_client -- --ignored --nocapture
/// ```
#[test]
#[ignore = "release-builds four egui binaries; run with -- --ignored"]
fn every_shipped_ui_tier_is_thin_and_only_the_named_one_embeds() {
    for (tier, bin) in THIN_TIERS {
        let Some(a) = audit(tier, bin, ServiceShape::ThinStandalone) else {
            panic!("'{tier}' ({bin}): built, but no artifact at the path — nothing was audited");
        };
        println!("{}", a.line());
        assert_ne!(
            a.verdict(),
            LinkageVerdict::NotMeasured,
            "'{tier}' ({bin}): linkage NOT MEASURED — an unchecked claim is not a passing audit"
        );
        assert!(
            a.holds(),
            "'{tier}' ({bin}) is declared THIN but LINKS {}.\n\
             A client that embeds its own server shows its own in-process world instead \
             of the server's — with nothing on screen saying which — and becomes a second \
             writer against the warehouse lock. Move whatever pulled it behind \
             `gui-embedded`, the tier that is asked for by name.",
            a.families()
        );
    }

    // …and the check discriminates. A guard that passed everything would be worth
    // nothing, so the fat tier must actually be fat: `gui-embedded` carries the
    // demo's `LocalHolger`, the bench's gateway and the warehouse's Iceberg engine.
    // The shared audit already fails an embedded variant that embeds NOTHING; holger
    // is stricter and wants all three of the split-out panes back.
    let a = audit(EMBEDDED_TIER.0, EMBEDDED_TIER.1, ServiceShape::FatService)
        .expect("the embedded tier must produce an artifact");
    println!("{}", a.line());
    assert!(
        a.holds(),
        "'{}' is the EMBEDDED tier and must actually embed: {}",
        EMBEDDED_TIER.0,
        a.line()
    );
    assert!(
        a.found.len() >= 3,
        "'{}' carries only {} — it is supposed to embed the demo's LocalHolger, the \
         bench's HTTP gateway AND the warehouse's Iceberg engine. If it stopped \
         embedding, the thin check above became vacuous and this file is no longer \
         proving anything.",
        EMBEDDED_TIER.0,
        a.families()
    );
}

/// **The guard's own guard, and it costs no build.** The three properties this file
/// leans on are the shared audit's, so they are asserted here directly rather than
/// trusted: a thin claim over holger's own server entry points fails, an unmeasured
/// audit never passes, and the tier list is not quietly empty.
#[test]
fn the_shared_audit_still_refuses_what_holger_needs_it_to_refuse() {
    // holger's two names ride alongside the shared families, and the shared families
    // do NOT contain a bare product name — `holger_server_lib` is linked by every
    // thin client and 481 of its symbols in `holger-server-ui-mini` are correct.
    let f = families();
    for name in HOLGER_SERVER_SYMBOLS {
        assert!(f.contains(&name), "{name} is one of holger's server entry points");
    }
    for legitimate in ["holger", "holger_server_lib", "RemoteHolger", "reqwest"] {
        assert!(
            !f.contains(&legitimate),
            "'{legitimate}' is CLIENT work — a guard on it would fail every tier there is"
        );
    }
    for shared in ["iceberg", "parquet", "tonic::transport::server"] {
        assert!(f.contains(&shared), "{shared} comes from the shared constellation list");
    }

    // An artifact that was never built is not an audit at all.
    assert!(
        LinkageAudit::of_families(
            "gui",
            ServiceShape::ThinStandalone,
            Path::new("/nonexistent-holger-server-ui"),
            &f,
        )
        .is_none(),
        "a missing binary yields no audit — the tier test turns that into a panic"
    );

    // A thin tier carrying holger's own in-process core handle is a lie, and a
    // measured-but-hollow embedded tier is a vacuous guard. Both must be red.
    let lying = LinkageAudit {
        variant: "gui".into(),
        artifact: PathBuf::from("target/release/holger-server-ui"),
        bytes: 64_733_488,
        declared: ServiceShape::ThinStandalone,
        found: vec![("LocalHolger".into(), 34), ("iceberg".into(), 5642)],
        // Glue-only families must NOT move a verdict — see the `glue_only` case below,
        // which is the actual regression this field was added for.
        glue_only: Vec::new(),
        measured: true,
    };
    assert_eq!(lying.verdict(), LinkageVerdict::ThinButLinks);
    assert!(!lying.holds(), "{}", lying.line());

    let hollow = LinkageAudit {
        declared: ServiceShape::FatService,
        found: Vec::new(),
        ..lying.clone()
    };
    assert_eq!(hollow.verdict(), LinkageVerdict::EmbeddedButEmpty);
    assert!(!hollow.holds(), "an embedded tier that embeds nothing makes the thin check vacuous");

    let unmeasured = LinkageAudit { measured: false, ..lying.clone() };
    assert_eq!(unmeasured.verdict(), LinkageVerdict::NotMeasured);
    assert!(!unmeasured.holds(), "an unchecked claim is not a verified one");

    // **The false red of 2026-08-01, pinned where it bit.** This tier test failed with
    // "gui: declared THIN but LINKS tonic::transport::server×1" on a single
    // `core::ptr::drop_glue::<…TowerToHyperService<…Server::serve_internal…>>` symbol —
    // retained destructor glue for a type constructed nowhere. Measured on that same
    // artifact: FastRoutes 0, LocalHolger 0, iceberg 0, parquet 0, and no non-glue
    // member of the tonic family at all. `edda`'s audit now counts non-glue symbols
    // only and reports glue separately; this asserts the VERDICT consequence, which is
    // the part holger depends on, rather than re-testing edda's counting rule.
    let glue_only_thin = LinkageAudit {
        declared: ServiceShape::ThinStandalone,
        found: Vec::new(),
        glue_only: vec![("tonic::transport::server".into(), 1)],
        ..lying.clone()
    };
    assert_eq!(
        glue_only_thin.verdict(),
        LinkageVerdict::ThinAndClean,
        "destructor glue is not an embedded server: {}",
        glue_only_thin.line()
    );
    assert!(glue_only_thin.holds(), "a thin tier is not a liar for carrying retained drop glue");
    // …and the evidence is still REPORTED, not discarded. A fix that hid it would have
    // traded a false red for a blind spot.
    assert_eq!(
        glue_only_thin.glue_only,
        vec![("tonic::transport::server".to_string(), 1)],
        "glue is reclassified, never dropped"
    );

    assert_eq!(THIN_TIERS.len(), 3, "three shipped client tiers; a shrinking list weakens this");
}