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

nordisk/nornir

src/lib.rs

//! # nornir
//!
//! A project-agnostic companion to `cargo`. Reusable building blocks
//! for release pipelines, benchmarks, and documentation written in
//! Rust, callable from thin per-project driver binaries.
//!
//! Modules:
//! - [`bench`]      benchmark harness + append-only JSONL history
//! - [`release`]    gates, publish-order walker, deploy
//! - [`deps`]       cross-crate dependency tracking (workspace-aware)
//! - [`docs`]       deterministic assembly of README / CHANGELOG / etc.
//! - [`guard`]      AI-agent guard rails: chmod -w forbidden paths
//!                  declared in `[guard].forbidden` of a nornir.toml
//! - [`introspect`] call graph + dep graph + public API extracted from
//!                  the *built* code (rustdoc JSON, cargo metadata, syn);
//!                  rendered into docs so they cannot drift from the code
//!
//! Optional features:
//! - `fixtures-maven` — canonical Maven Central JAR set as bench inputs
//!
//! Future companion crates (not in this repo):
//! - `nornir-mcp` — exposes every API here as an MCP (Model Context
//!   Protocol) tool, letting an LLM agent drive build/release/docs.
//! - `nornir-cli` — `cargo nornir ...` subcommand for human use.
//!
//! See `workspace_holger/release/SPEC.md` for the contract that
//! generated binaries implement on top of this library.

// The viz app's whole-app `state_json()` is one large `serde_json::json!`
// object literal (one tab pane per key); each entry costs a level of the
// `json_internal!` macro recursion, so the default limit of 128 is exceeded once
// the tab set grows. 256 gives ample headroom for the pane keys.
#![recursion_limit = "256"]

// Self-alias so modules that are `#[path]`-shared with the integration-test
// crates (where the library is an EXTERNAL `nornir::` dependency) — notably the
// synthetic-workspace generator included by [`bench_registry`] — resolve their
// `nornir::…` paths identically when compiled INTO this crate. Without it,
// `nornir::gitio` is unresolvable inside the lib (a crate is not a dependency of
// itself).
extern crate self as nornir;

/// Short git commit hash the binary was built from, stamped by `build.rs` at
/// build time via **pure `std::fs`** (no shellout — the zero-shell law). `env!`
/// (not `option_env!`) because `build.rs` emits `NORNIR_GIT_HASH` on every
/// build; `unknown` off a git checkout.
pub const GIT_HASH: &str = env!("NORNIR_GIT_HASH");

/// The canonical version-identity line: `v<CARGO_PKG_VERSION> (<git hash>)`.
/// The ONE source of truth for nornir's identity string — printed by
/// `nornir --version`, returned as `Health.Ping.version`, and surfaced in the
/// viz About panel. Keep every identity surface pointed here so they can never
/// drift.
pub const VERSION_LINE: &str = concat!(
    "v",
    env!("CARGO_PKG_VERSION"),
    " (",
    env!("NORNIR_GIT_HASH"),
    ")"
);

/// **Which ATOM this build IS**, read back off the features that actually change
/// the artifact — the applied proof that the atom selector selects.
///
/// `.nornir/build-variants.toml` declares FIVE atoms, two of which
/// (`nornir` / `nornir-cli`) are the SAME source file and two of
/// which (`nornir-ui` / `nornir-solo`) are another. Before 2026-08-01 all three
/// CLI atoms even emitted the same `bin` name, so nothing on disk — and nothing a
/// running process could say — distinguished them. Distinct `[[bin]]` names fixed
/// the file collision; this function closes the other half: run the binary and it
/// TELLS you which atom it is.
///
/// The two discriminating features are real:
///   * `embed-holger` — the one optional sibling nornir has (`fat` vs `mixed`);
///   * `solo`         — gates the in-process `server_grpc` boot in the GUI bin.
///
/// `atom-cli` is a pure MARKER (empty feature value): it exists so the `nornir-cli`
/// `[[bin]]` can have a `required-features` key of its own, and it is read here so
/// that marker is never vacuous. `skade` is reported unconditionally because it is
/// a PLAIN path dependency — there is no nornir build in which it is not linked in
/// (which is also why no nornir atom may claim `linkage = "thin"`).
///
/// # Why the atom name is a PARAMETER and not a `cfg!`
///
/// It used to be `cfg!(feature = "solo") … else if cfg!(feature = "thin") …` (the
/// marker was spelled `thin` then), and
/// that could not work. **Cargo unifies features across every bin of one package in
/// one invocation.** `cargo test --features atom-cli` builds `nornir` AND `nornir-cli`
/// off the one source with `atom-cli` ON for both, so a feature-derived name reports
/// `nornir-cli` from both binaries and the marker marks nothing. Measured RED on
/// 2026-08-01, `nornir --version` vs `nornir-cli --version` under that exact
/// command:
///
/// ```text
/// left:  "nornir v0.5.4 (339d6b2) — atom nornir-cli · linkage fat · holger=embedded …"
/// right: "nornir v0.5.4 (339d6b2) — atom nornir-cli · linkage fat · holger=embedded …"
/// ```
///
/// The same defect reached a shipped artifact by the other route: `nornir-ui`'s
/// declared feature set names none of the three discriminators, so the GUI atom
/// called itself `atom nornir` — the CLI's name.
///
/// `CARGO_BIN_NAME` is per-BIN and cargo cannot unify it, so the caller passes
/// `env!("CARGO_BIN_NAME")` and a binary can only ever report the atom it IS.
///
/// The REST of the line stays `cfg!`-derived, and correctly so: `linkage`,
/// `holger` and `server` describe the compilation this process came out
/// of, which is exactly what they claim to. In a unified test build every bin of
/// that build reports the same ones, because every bin of that build really does
/// have them. The per-atom difference is a property of the DECLARED feature sets in
/// `.nornir/build-variants.toml` and is proven by building the atoms — see
/// `.nornir/suite-atoms-design.md`.
///
/// Asserted by `tests/build_variants.rs::the_atom_marker_reaches_the_binary`, which
/// runs the built `nornir`, `nornir-cli` and `nornir-ui` under the hostile unified
/// build and requires each to name itself.
pub fn atom_line(bin: &str) -> String {
    let atom = bin;
    let linkage = if cfg!(feature = "embed-holger") {
        "fat"
    } else {
        "mixed"
    };
    let holger = if cfg!(feature = "embed-holger") {
        "embedded"
    } else {
        "remote"
    };
    let server = if cfg!(feature = "server") {
        "in-process"
    } else {
        "remote"
    };
    format!(
        "atom {atom} · linkage {linkage} · holger={holger} · skade=embedded · \
         server={server}"
    )
}

/// `VERSION_LINE` plus [`atom_line`] — what every nornir launchable prints for
/// `--version`. One writer, so the CLI, the daemon, the GUI and the solo appliance
/// can never disagree about which build they are.
/// `&'static str` (via a `OnceLock`, not a leak per call) because clap's
/// `#[command(version = …)]` takes `clap::builder::Str`, which is `From<&'static
/// str>` but not `From<String>`.
/// `bin` is the caller's `env!("CARGO_BIN_NAME")` — see [`atom_line`] for why it
/// cannot be read from a `cfg!` here. One `OnceLock` per PROCESS is right because a
/// process is exactly one binary, so the first call's name is the only name.
pub fn version_full(bin: &str) -> &'static str {
    static CELL: std::sync::OnceLock<String> = std::sync::OnceLock::new();
    CELL.get_or_init(|| format!("{VERSION_LINE} — {}", atom_line(bin)))
        .as_str()
}

pub mod arch;
pub mod autonom;
// nornir's OWN benchers, registered via `register_bench!` so `inventory` links
// them into the lib (and thus into the completeness-gate binary). See the module
// docs: the discovery seam `nornir_bench::api::registered_bench_ids()` can only
// see benchers that are LINKED, so the registrations must live here, not in the
// `examples/nornir-bench.rs` runner (an example is not linked into the lib).
mod bench_registry;
// Mimir POPULATE + SEARCH bench suite (~100 benchers) — same linkage rationale as
// `bench_registry`: the registrations must compile INTO the lib.
mod bench_mimir;
// Lifted into the `nornir-bench` leaf crate (its cli_outcome coupling is via
// nornir-cli-outcome; the live progress trail writes $NORNIR_VIZ_ACTIONLOG
// directly, so no viz/egui dep is pulled in); re-exported (with the
// `register_bench!` / `register_bench_ordered!` macros) so every
// `crate::bench::…` call site resolves unchanged.
pub use nornir_bench as bench;
pub use nornir_bench::{register_bench, register_bench_ordered};
pub mod binscan;
pub mod build;
// Host wiring for the `brokkr` CI/build-server core: the CI_BUILD job
// orchestrator + history store + the DemoRunner. The pipeline model/DAG/engine
// live in the dep-light `brokkr` leaf crate; this is the nornir-only glue.
pub mod change;
// The ONE byte-budget policy every in-process cache with unbounded-size entries
// routes through (`vector::store`'s index cache, `knowledge`'s view cache). A count cap bounds nothing when one entry is a whole table.
pub mod cache_budget;
// Lifted into the `nornir-cli-outcome` leaf crate (pure serde); re-exported so
// every `crate::cli_outcome::…` call site resolves unchanged.
pub use nornir_cli_outcome as cli_outcome;
// Lifted into the `nornir-config` edda leaf crate (nornir.toml loader + the
// $HOME-derived data-root rule; migration-ledger §N prereq) so dwarves can
// consume it FAT without a nornir->dwarves cycle. Re-exported so every
// `crate::config::…` / `nornir::config::…` call site resolves unchanged.
pub use nornir_config as config;
/// The `[repo.x]` vs `[repos.x]` REFUSAL at the seam where nornir calls the edda
/// loaders (`nornir-config`, `nornir-workspace`) — one letter apart, and the
/// loaders were silent about it (INVENTORY-semantic-twins-2026-09-10 §5 #6).
pub mod config_spelling;
// Lifted into the `nornir-coverage` leaf crate (cargo-llvm-cov run/report, pure
// serde; its warehouse/module couplings were doc-links only — the Iceberg
// persistence stays in nornir); re-exported so every `crate::coverage::…` call
// site resolves unchanged.
pub use nornir_coverage as coverage;
/// The ureq 3 agent nornir's own HTTP calls use, with ureq 2's defaults so the bump
/// (2026-10-02) changes no behaviour: no proxy from the environment and at most 5
/// redirects (ureq 2 without `proxy-from-env`; ureq 3 reads the env and allows 10).
/// Per-call deadlines go on the request (`.config().timeout_global(..)`).
pub(crate) fn http_agent() -> ureq::Agent {
    ureq::Agent::config_builder().proxy(None).max_redirects(5).build().new_agent()
}

/// **The code room of a workspace** (Mímir v2): the handle every front door
/// holds, and the reader dispatch of everything that is not a lens. See
/// [`code_room`] for the seam.
pub mod code_room;
pub mod coderoom;
pub mod deepscan;
pub mod deps;
pub mod docs;
/// The FROZEN, append-only `nornir-<subsystem>-<n>` application error codes — the
/// machine half of every non-UI error nornir emits (the human string is the other
/// half, and both ride in the same message). See `.nornir/nornir-error-codes.md`.
pub mod errcode;
/// **nornir's REACTION to a facett UI error code** (facett error-codes, Phase 2) —
/// [`facet_react::VIZ`] namespaces a `facet-<component>-<n>` with nornir's mount
/// (`nornir/viz:facet-graph3d-1`) and returns the canonical reaction. COMPOSES with
/// [`errcode`] (`nornir-<subsystem>-<n>`), never collides. `viz`-gated: it needs the
/// facett-core dependency the viz feature pulls in.
#[cfg(feature = "viz")]
pub mod facet_react;
pub mod funnel;
/// Graph extraction — the one `G = (V, E)` model fed by both a source extractor
/// (manifests / SCIP) and a **binary extractor** ([`graph::extract::binary`],
/// ELF → `G`) for the airgap / vendored-`.so` case (EPIC item 8).
pub mod graph;
// Lifted into the `nornir-git` leaf crate (gix + russh transport, no warehouse/
// embedder deps); re-exported so every `crate::gitio::…` / `crate::gitclean::…` /
// `crate::ssh::…` call site resolves unchanged.
pub use nornir_git::{gitclean, gitio, gitops, ssh};
// Lifted into the `nornir-guard` leaf crate (anyhow + sha2, its only cross-crate
// coupling is nornir-cli-outcome); re-exported so every `crate::guard::…` call
// site resolves unchanged.
pub use nornir_guard as guard;
#[cfg(feature = "embed-holger")]
pub mod holger_embed;
pub mod index;
pub mod introspect;
// Lifted into the `nornir-jobs` leaf crate (redb ledger + serde; its warehouse
// couplings were doc-links only — the concrete Iceberg persistence stays in
// nornir); re-exported so every `crate::jobs::…` call site resolves unchanged.
/// The `Cargo.toml` FEATURE-ACTIVATION parser — the producer behind the
/// `feature_dep_edges` warehouse table (populate step 7d), re-exported so
/// consumers reach the parse, the row TYPE and the two adjoints through nornir
/// instead of taking a second dependency that could resolve to another version.
/// The warehouse writer/reader is `warehouse::feature_dep_edges`.
pub use nornir_feature_graph as feature_graph;
pub use nornir_jobs as jobs;
pub mod knowledge;
/// **The MCP stdio server**, served by `nornir-server mcp`.
///
/// Was `src/bin/nornir-mcp.rs` until 2026-08-03. It is a library module now
/// because the build matrix gives nornir five atoms and `nornir-mcp` is not one
/// of them: MCP is a capability of the `fat` server ("everything compiled in"),
/// not a delivery beside it. See the module docs for why it is a SUBCOMMAND of
/// the server binary rather than a gRPC service — MCP is stdio, and a daemon
/// holding a port has no client stdin to read.
#[cfg(feature = "mcp")]
pub mod mcp_stdio;
pub mod mimir;
pub mod mimir_analyze;
/// **The calibration oracle** — what a cosine threshold is WORTH in the
/// embedding space that is actually loaded. Measures constructed Type-1/2/3
/// clone pairs against hard negatives (same signature, different body), derives
/// `cosine_floor = μ_pos − k·σ_pos`, and REFUSES to certify a space it cannot
/// tell clones apart in. See [`mimir_calibrate`].
pub mod mimir_calibrate;
/// Type-4 semantic clones — independent re-implementations of one behaviour:
/// HIGH embedding cosine with LOW structural agreement (the dup gate inverted).
pub mod mimir_clones;
/// **SubClones** — duplicate code INSIDE items, found by winnowing over a
/// Type-2-normalised token stream. Every other detector in the constellation
/// keys on a chunk, and a chunk is an item, so a block shared between two
/// otherwise-different functions is invisible to all of them at every
/// threshold. See [`mimir_subclone`].
///
/// ⚠ **Gated on `vector`** — it imports [`vector::spans`] for the enclosing-item
/// label. WITHOUT this gate the module was declared unconditionally while its
/// import was conditional, so `cargo check -p nornir --no-default-features
/// --features full-server` (nornir is a PATH DEP of dwarves, built continuously
/// under exactly that line) failed with `E0433: cannot find vector in crate` at
/// `mimir_subclone.rs:111`. `full-server = ["server"]` and does not imply
/// `vector`. The alternative — adding `vector` to `full-server` — was MEASURED
/// and rejected (it exposed a second gap in that feature combination at the
/// time). Gating costs nothing (`vector` is in `default`) and does not
/// move the feature graph under the other lanes.
#[cfg(feature = "vector")]
pub mod mimir_subclone;
/// **Group twins** — the same semantic GROUP of statements inside two
/// different functions, found even when the copy is INTERLEAVED with noise
/// lines: the case winnowing's contiguous-run unit makes unreportable by
/// construction (the module carries the measured blindness test). Def-use
/// segmentation, the `semantic_clones` normalisation per group, vectors in
/// `embeddings_groups`.
///
/// Gated on `vector` for the same reason [`mimir_subclone`] is.
#[cfg(feature = "vector")]
pub mod mimir_groupclone;
/// **Group twins from the compiled-code graph** — the same group of
/// statements in two different functions, read from the code room's
/// per-line compiled facts (resolved callees, fields, tables, let types),
/// weighted by rarity. No model, no vectors. The `group_twins` lens's
/// primary signal.
#[cfg(feature = "vector")]
pub mod mimir_groupclone_graph;
/// The clone signals — clones by what a function DOES, not what it is called:
/// the normalised body (exact key + MinHash, `exact_clones` / `near_clones`),
/// the structure in the graph (`structural_clones`), and the model over the
/// normalised body (`semantic_clones`).
#[cfg(feature = "vector")]
pub mod clone_signals;
/// **The twin scan** — the caller [`mimir_subclone`] never had, and the GPU
/// precondition in front of the dependency sweep. Own code is a cheap CPU fold
/// that runs on every generation change; deps is a GPU-gated job that runs only
/// when the resolved dependency closure moves. See [`twinscan`].
///
/// Gated on `vector` because [`mimir_subclone`] is — see above.
#[cfg(feature = "vector")]
pub mod twinscan;
/// SemanticClones STAGE 2 — call-chain k-hop subgraph slices, serialised
/// order-independently and embedded under the synthetic `<repo>::slices`
/// namespace, so re-implementations SPLIT across N helpers become findable.
#[cfg(feature = "vector")]
pub mod mimir_slices;
/// The QUERY half of the Mimir lenses (bloat/zombie/risk/weak/bench-gaps/
/// similar-code), lifted from `dwarves::mimir` behind the `Mimir` gRPC service.
pub mod mimir_lens;
pub mod monitor;
/// Reap the `sh -c "git-lfs filter-process"` children `gix-filter` leaks on every
/// worktree checkout — measured at ~1 330/h on the live server.
pub mod procreap;
pub mod registry;
pub mod security;
// Lifted into the `nornir-selftest` leaf crate (bridges nornir-testmatrix);
// re-exported (with the `assert_emit!` macro) so every `crate::selftest::emit`
// and `assert_emit!` call site resolves unchanged.
pub use nornir_selftest as selftest;
pub use nornir_selftest::assert_emit;
// `ssh` moved into the `nornir-git` crate (re-exported above with gitio/gitclean).
pub mod release;
pub mod robot;
/// Saga record/replay over gRPC — the remote ingest + read-back endpoint
/// ([`saga_grpc::SagaGrpcService`]) a thin producer ships to, and the client-side
/// [`saga_grpc::GrpcSagaStore`] that forwards a recorder's flush to it. Present
/// whenever the tonic wire is compiled (same gate as [`warehouse::remote`]).
#[cfg(any(feature = "server", feature = "viz", feature = "mcp"))]
pub mod saga_grpc;
pub mod test_matrix;
/// Incremental tests, the graph-free half: what a commit changed (`Test.Affected`).
pub mod test_select;
#[cfg(feature = "vector")]
pub mod vector;
pub mod warehouse;
// The full nornir gRPC server stack (all 20 services + workspace registry + monitor
// poll loop), lifted out of `src/bin/nornir-server.rs` so BOTH the `nornir-server`
// bin AND the fat `dwarves-full` bin call the SAME `server_grpc::run` (REUSE law — no
// twinned service registration). Same `server` gate as the wire it builds.
#[cfg(feature = "server")]
pub mod server_grpc;
// `viz` builds the full egui app; `server` needs only `viz::model` (the pure
// timeline builder) for the `Viz.Timeline` RPC — submodules are gated inside.
#[cfg(any(feature = "viz", feature = "server"))]
pub mod viz;
// The multi-repo workspace descriptor (`nornir-workspace.toml` parse + repo-source
// resolve) was lifted into the `nornir-workspace` edda leaf so dwarves can consume
// it FAT without inverting edda->nornir into a cycle (it's warehouse-free — pure
// parse + resolve over the `nornir-git` transport). Re-exported verbatim so every
// `crate::workspace::…` / `nornir::workspace::…` consumer compiles unchanged.
pub use nornir_workspace as workspace;

// `nornir login` — the CLIENT side of holger-style OIDC auth (RFC 8628 device-code SSO),
// built on the SHARED `nornir-auth-client` (edda) so login works identically across the
// constellation (L5 — never a device-code twin). Behind `--features login`; the lean
// build links none of the auth/reqwest stack. See `src/login.rs`.
#[cfg(feature = "login")]
pub mod login;

/// The cached OIDC bearer (from `nornir login`), read WITHOUT any network, or `None` when
/// signed out or the `login` feature is not compiled. ALWAYS compiled so the CLI / saga /
/// MCP client bearer-resolution sites can PREFER an OIDC token over the static
/// `~/.nornir/token` / `$NORNIR_SERVER_TOKEN` without a per-site `cfg` — the lean build
/// gets a `None` that folds away, keeping the old token path as the sole fallback.
pub fn cached_oidc_bearer() -> Option<String> {
    #[cfg(feature = "login")]
    {
        login::cached_oidc_token().filter(|t| !t.is_empty())
    }
    #[cfg(not(feature = "login"))]
    {
        None
    }
}

/// **The bearer a thin client should present, and the rung that supplied it** — the ONE
/// place `nornir`'s client-side credential ladder is walked.
///
/// Rungs, highest first: `--token` → `$NORNIR_SERVER_TOKEN` → a cached OIDC access token
/// (`nornir login`, read with NO network) → the static `<nornir home>/token`. The first
/// three come from the SHARED `nornir_service::resolve_token_whence` (the same helper
/// korp and dwarves reuse — LAW 5, never a hand-rolled `read_to_string`+`trim` copy of
/// its bottom rung); OIDC is nornir's own extra rung, and it is inserted where it
/// belongs rather than bolted on: it outranks the static FILE and is outranked by the
/// flag and the env var, so it applies exactly when the shared walk reached the file
/// rung or ran out.
///
/// # The defect this exists to close (2026-08-27)
///
/// `src/bin/nornir.rs` seeded `$NORNIR_SERVER_TOKEN` only when **`cli.server.is_some()`**
/// — the FLAG. So the file and OIDC rungs were walked for `nornir jobs --server <url>`
/// and NOT for `NORNIR_SERVER=<url> nornir jobs`, even though `server_target()` honours
/// both and the flag's own handler had already normalised it into that same env var.
/// Measured live: the env form answered `Unauthenticated`, the flag form returned rows.
/// Two ways to name the same server, one of them unauthenticated — and the client
/// reported the server's refusal rather than its own missing walk.
///
/// `target` is therefore the **resolved** server target (flag or `$NORNIR_SERVER`), not
/// the flag: `None` ⇒ no server to authenticate to ⇒ no rung is walked at all.
///
/// **PURE apart from the env rung the shared resolver owns**, and every other input is a
/// parameter — so the RED arm is provable without a process and without a live server.
/// A test that inferred the rung by comparing the resolved VALUE against `$VAR` would be
/// worthless here: all four token files on oden hold byte-identical secrets
/// (sha256 `6343b883…`), so such a comparison answers "env" whichever rung actually
/// replied. The rung is returned as a fact from the walk.
#[cfg(feature = "wire-client")]
pub fn client_bearer(
    flag: Option<&str>,
    target: Option<&str>,
    token_file: Option<&std::path::Path>,
    oidc: Option<&str>,
) -> Option<(String, nornir_service::TokenSource)> {
    use nornir_service::TokenSource;
    // An explicit `--token` is honoured with or without a server target: the operator
    // said so, and it was unconditional before this change — gating it would alter
    // behaviour this fix has no business altering. Only the LADDER below is gated.
    if let Some(t) = flag.map(str::trim).filter(|t| !t.is_empty()) {
        return Some((t.to_string(), TokenSource::Flag));
    }
    // Every other rung needs a server to authenticate TO. **This is the line that was
    // wrong**: it read the FLAG (`cli.server.is_some()`), so the env form of the very
    // same target got no walk at all.
    target.filter(|t| !t.trim().is_empty())?;
    let (token, whence) =
        nornir_service::resolve_token_whence(None, "NORNIR_SERVER_TOKEN", token_file);
    // OIDC sits between the env var and the static file, so it only gets a look when the
    // shared walk fell through to the file or found nothing.
    if matches!(whence, TokenSource::File(_) | TokenSource::Nothing) {
        if let Some(t) = oidc.map(str::trim).filter(|t| !t.is_empty()) {
            return Some((t.to_string(), TokenSource::Oidc));
        }
    }
    (!token.is_empty()).then_some((token, whence))
}

pub use anyhow::{Error, Result};

/// Process-wide serial lock for tests that mutate GLOBAL state (env vars, cwd, or
/// spawn `cargo metadata`). `cargo test` runs tests multi-threaded by default, so
/// two tests touching the same `HOME`/`NORNIR_VIZ_ACTIONLOG`/`cargo`-lock race
/// intermittently (each passes alone). Such tests take `let _g = nornir::serial_guard();`
/// at the top so they run one-at-a-time relative to each other. Poison-tolerant.
#[cfg(test)]
pub(crate) fn serial_guard() -> std::sync::MutexGuard<'static, ()> {
    static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
    LOCK.lock().unwrap_or_else(|e| e.into_inner())
}

#[cfg(all(test, feature = "wire-client"))]
mod client_bearer_tests {
    use super::client_bearer;
    use nornir_service::TokenSource;

    /// The env rung is read by the shared resolver out of the real process environment,
    /// so a test that leaves `$NORNIR_SERVER_TOKEN` set is testing something else. Say so
    /// rather than pass for the wrong reason.
    fn assert_env_is_clean() {
        assert!(
            std::env::var_os("NORNIR_SERVER_TOKEN").is_none(),
            "$NORNIR_SERVER_TOKEN is set in this test process, so the env rung answers \
             before the file rung and this test cannot see what it is for. Unset it."
        );
    }

    /// ★ **RED-when-broken: the ladder walks for `$NORNIR_SERVER`, not only for
    /// `--server`.**
    ///
    /// The defect verbatim: the gate was `cli.server.is_some()`, the FLAG. Here the
    /// target arrives the way `$NORNIR_SERVER` delivers it — a plain string, with no flag
    /// anywhere — and the file rung must still answer.
    ///
    /// Asserted on the RESOLVED VALUE against a token file this test wrote, and on the
    /// RUNG as a fact from the walk. The value assertion is what makes it real: on oden
    /// all four token files hold byte-identical secrets (sha256 `6343b883…`), so a test
    /// that compared the answer against `$NORNIR_SERVER_TOKEN` would report "env" whether
    /// the walk worked or not. This file's secret is unique by construction.
    #[test]
    fn the_token_file_rung_answers_for_an_env_supplied_target() {
        assert_env_is_clean();
        let dir = tempfile::tempdir().expect("tempdir");
        let file = dir.path().join("token");
        std::fs::write(&file, "  file-rung-secret-unique-to-this-test\n").expect("write token");

        // (1) The ENV form — no flag at all. This is the case that was broken.
        let (tok, whence) = client_bearer(None, Some("http://127.0.0.1:7878"), Some(&file), None)
            .expect(
                "an env-supplied target with a readable token file MUST yield a bearer — \
                 this is the defect: the walk was gated on `cli.server.is_some()`, so \
                 `NORNIR_SERVER=… nornir jobs` went out unauthenticated while \
                 `nornir jobs --server …` did not",
            );
        assert_eq!(tok, "file-rung-secret-unique-to-this-test", "trimmed, from the FILE");
        assert_eq!(
            whence,
            TokenSource::File(file.clone()),
            "and the rung is the file, as a fact from the walk — never inferred by \
             comparing the value against $NORNIR_SERVER_TOKEN, which on this box would \
             answer `env` either way"
        );

        // (2) The FLAG form must give the IDENTICAL answer. Two ways to name one server
        //     is one credential; that they agree is the whole property.
        let flagged = client_bearer(None, Some("127.0.0.1:7878"), Some(&file), None)
            .expect("the same target by any spelling");
        assert_eq!(flagged, (tok, whence), "flag form and env form resolve identically");
    }

    /// **RED-when-broken: no server target ⇒ no ladder walk.** The gate is not deleted,
    /// it is corrected: a purely local `nornir` run must not go reading token files.
    /// Without this the fix would be "walk always", which is a different change.
    #[test]
    fn no_server_target_walks_no_rung() {
        assert_env_is_clean();
        let dir = tempfile::tempdir().expect("tempdir");
        let file = dir.path().join("token");
        std::fs::write(&file, "should-not-be-read").expect("write token");

        assert!(
            client_bearer(None, None, Some(&file), None).is_none(),
            "no target ⇒ nothing to authenticate to ⇒ no rung"
        );
        assert!(
            client_bearer(None, Some("   "), Some(&file), None).is_none(),
            "a blank target is not a target"
        );

        // …but an explicit `--token` is still honoured with no target, exactly as before.
        assert_eq!(
            client_bearer(Some("explicit"), None, Some(&file), None),
            Some(("explicit".to_string(), TokenSource::Flag)),
            "an explicit --token is the operator's word and was unconditional before \
             this change; only the LADDER is gated on a target"
        );
    }

    /// **RED-when-broken: OIDC sits BETWEEN the env var and the static file, and reports
    /// itself as OIDC.**
    ///
    /// The rung matters because the repair differs: a flag is something to retype, a
    /// file is something to go and write, an expired SSO session is something to `login`
    /// again. Calling an OIDC token `TokenSource::Flag` sends an operator hunting for a
    /// `--token` nobody passed.
    #[test]
    fn oidc_outranks_the_static_file_and_is_named_as_itself() {
        assert_env_is_clean();
        let dir = tempfile::tempdir().expect("tempdir");
        let file = dir.path().join("token");
        std::fs::write(&file, "file-secret").expect("write token");

        let (tok, whence) = client_bearer(
            None,
            Some("http://127.0.0.1:7878"),
            Some(&file),
            Some("oidc-access-token"),
        )
        .expect("a bearer");
        assert_eq!(tok, "oidc-access-token", "OIDC outranks the static file");
        assert_eq!(whence, TokenSource::Oidc, "…and says so — not `Flag`, not `File`");

        // An explicit flag still outranks OIDC.
        let (tok, whence) = client_bearer(
            Some("flag-secret"),
            Some("http://127.0.0.1:7878"),
            Some(&file),
            Some("oidc-access-token"),
        )
        .expect("a bearer");
        assert_eq!((tok.as_str(), whence), ("flag-secret", TokenSource::Flag));

        // A signed-out/lean build (`None`) falls through to the file, unchanged.
        let (tok, whence) =
            client_bearer(None, Some("http://127.0.0.1:7878"), Some(&file), None).expect("a bearer");
        assert_eq!((tok.as_str(), whence), ("file-secret", TokenSource::File(file)));

        // A blank OIDC cache is not a credential — it must not shadow the file.
        let dir2 = tempfile::tempdir().expect("tempdir");
        let f2 = dir2.path().join("token");
        std::fs::write(&f2, "file-secret-2").expect("write");
        let (tok, _) =
            client_bearer(None, Some("http://x:1"), Some(&f2), Some("   ")).expect("a bearer");
        assert_eq!(tok, "file-secret-2", "a whitespace OIDC cache is signed out");
    }
}