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

nordisk/nornir

tests/viz_jobs_headless.rs

//! **The jera ledger over the wire, on the build the FORGE actually ships.**
//!
//! `#![cfg(all(feature = "server", not(feature = "viz")))]` is the entire point of
//! this file. Every other server test in this repo runs `--features viz,server`, and
//! that is the configuration the bug could not appear in.
//!
//! # The red, measured on oden 2026-08-27
//!
//! The fat forge embeds this lib as `nornir { default-features = false, features =
//! ["full-server"] }`, and `full-server = ["server"]` names no `viz`. The `Viz`
//! service was `#[cfg(feature = "viz")]`, so the daemon mounted no `Viz` at all:
//!
//! ```text
//! $ nornir jobs --server http://127.0.0.1:7878 --workspace nordisk --json
//! Error: Viz.Jobs RPC
//! Caused by: code: 'Operation is not implemented or not supported'
//!
//! $ nornir jobs --server http://100.115.167.95:7900 --workspace nordisk --json
//! Error: Viz.Jobs RPC
//! Caused by: code: '…not implemented…', message: "dwarves-proxy-3: "
//! ```
//!
//! — on dwarves `aa07395`, built 10:46, started 11:05, i.e. NOT a stale binary. The
//! second line is the forge's own proxy faithfully relaying its upstream's refusal:
//! the trimmed `viz.proto`, the thin `nornir-warehouse-embed::viz` client and the
//! forge's `VizProxy` were all built and wired, and the last hop was gated on a
//! window nobody was opening. Fixed by moving the gate off the service and onto the
//! one handler (`Knowledge`) that truly needs the egui-side crate.
//!
//! # Why the assertion is a SENTINEL row and not "it answered"
//!
//! An empty ledger answers `[]`, and `[]` is the shape of a working server, a wrong
//! workspace and a dropped write alike. So the test WRITES first — a `job_id` no
//! local read could synthesise — over `Telemetry.SubmitJobs`, then reads it back over
//! `Viz.Jobs` and asserts the field values survived the round trip. It asserts the
//! sentinel is ABSENT before the write rather than that the ledger is empty: measured
//! here, a freshly booted server already holds ~16 rows of its OWN work (the registry
//! fetch and snapshot jobs it opens at boot), which is itself the applied-output
//! answer to "does the server write the ledger at all".
//!
//! Run: `cargo test --no-default-features --features server --test viz_jobs_headless`

#![cfg(all(feature = "server", not(feature = "viz")))]

use nornir::jobs::{JobRecord, kind, status};
use nornir::viz::remote;

#[path = "support/ephemeral_server.rs"]
mod ephemeral_server;

const WS: &str = "vizjobs";

/// NOW, in micros. Not a fixed epoch: `JobStore::submit` prunes terminal rows past
/// retention **inside the same call**, so a row stamped 2023 is accepted (`accepted: 1`)
/// and deleted before it can ever be read back. That cost this test two runs, and it is
/// worth writing down — "the write reported success" and "the row is there" are two
/// different facts on this API.
fn now_micros() -> i64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_micros() as i64
}

fn sentinel(job_id: &str) -> JobRecord {
    JobRecord {
        job_id: job_id.to_string(),
        kind: kind::BENCH_RUN.to_string(),
        target: "nornir@headless".to_string(),
        workspace: WS.to_string(),
        status: status::DONE.to_string(),
        ts_start_micros: now_micros(),
        ts_end_micros: Some(now_micros() + 1_000_000),
        elapsed_ms: Some(1_000),
        detail_json: String::new(),
        result_ref: String::new(),
        parent_id: None,
        node: None,
        phase: None,
        actor: None,
        actor_method: None,
    }
}

/// ★ THE GUARD. A headless `server` build must SERVE `Viz.Jobs`, and serve the row
/// that was written — not `Unimplemented`, and not an empty list.
#[test]
fn a_headless_server_serves_the_job_ledger_over_viz_jobs() {
    let srv = ephemeral_server::spawn(WS, "vizjobs");

    let id = format!("sentinel-{}-viz-jobs", std::process::id());

    // ★ THE READ ITSELF IS THE GUARD. Before the fix this `expect` fired with
    // `Unimplemented` — the service was gated on the GUI feature, so a headless daemon
    // mounted no `Viz` at all. The sentinel must be absent here so that finding it below
    // can only mean the write leg landed it.
    let before = remote::fetch_jobs(&srv.endpoint, &srv.token, WS, "", 0)
        .expect("Viz.Jobs must be SERVED on a headless build — this is the bug this file guards");
    assert!(
        !before.iter().any(|r| r.job_id == id),
        "the sentinel cannot already be in the ledger"
    );
    let accepted = remote::submit_jobs(&srv.endpoint, &srv.token, WS, &[sentinel(&id)])
        .expect("Telemetry.SubmitJobs");
    assert_eq!(accepted, 1, "the server accepted no job record");

    let rows = remote::fetch_jobs(&srv.endpoint, &srv.token, WS, "", 0).expect("Viz.Jobs");
    let got = rows
        .iter()
        .find(|r| r.job_id == id)
        .unwrap_or_else(|| panic!("sentinel `{id}` absent from {} ledger rows", rows.len()));

    // APPLIED OUTPUT, field by field — a round trip that only preserved the id would
    // pass a serializer that dropped every other column.
    assert_eq!(got.kind, kind::BENCH_RUN, "kind lost over the wire");
    assert_eq!(got.target, "nornir@headless", "target lost over the wire");
    assert_eq!(got.workspace, WS, "workspace lost over the wire");
    assert_eq!(got.status, status::DONE, "status lost over the wire");
    assert_eq!(got.elapsed_ms, Some(1_000), "elapsed_ms lost over the wire");
}

/// The `kind` filter is the argument the 🧬 Jera tab passes, so it has to select and
/// not merely be accepted. A server that ignored it would still pass the test above.
#[test]
fn the_kind_filter_selects_rather_than_being_ignored() {
    let srv = ephemeral_server::spawn(WS, "vizjobskind");

    let bench = format!("bench-{}", std::process::id());
    let mut docs = sentinel(&format!("docs-{}", std::process::id()));
    docs.kind = kind::DOCS_RENDER.to_string();
    remote::submit_jobs(
        &srv.endpoint,
        &srv.token,
        WS,
        &[sentinel(&bench), docs.clone()],
    )
    .expect("Telemetry.SubmitJobs");

    let only_docs = remote::fetch_jobs(&srv.endpoint, &srv.token, WS, kind::DOCS_RENDER, 0)
        .expect("Viz.Jobs (filtered)");
    assert!(
        only_docs.iter().any(|r| r.job_id == docs.job_id),
        "the filtered read dropped the row it was asking for; got {:?}",
        only_docs.iter().map(|r| (&r.job_id, &r.kind)).collect::<Vec<_>>()
    );
    assert!(
        !only_docs.iter().any(|r| r.job_id == bench),
        "the `kind` filter was accepted and IGNORED — the bench row came back under \
         a docs_render filter"
    );
}