nornir 0.4.21

Companion to cargo: dependency tracking, release gating, deploy, benchmarks, and documentation assembly. Project-agnostic.
Documentation
//! Inject-and-assert test for the viz **Knowledge** tab (plan item C2, LAW #1).
//!
//! Feeds a *known* small on-disk workspace (two tiny crates we wrote, with a
//! counted number of `fn`/`struct` items and call expressions) into the
//! Knowledge tab, runs the scan, and asserts the resulting
//! `state_json()["knowledge"]` carries the **expected symbol/call counts** — not
//! merely "it didn't panic". Then it asserts a workspace switch resets + rescopes
//! the tab to the new repo set.
//!
//! Why on-disk crates (not a mock): the Knowledge scan is a real `syn` parse of
//! every `.rs` under each repo, so the only honest way to assert counts is to
//! parse source we control and compare. The scan is run synchronously via the
//! `*_for_test` shim (no GUI loop to drain the background slot); production code
//! runs the identical scan on a background thread so the UI never blocks.
#![cfg(feature = "viz")]

use std::fs;
use std::path::Path;

use nornir::viz::UrdrThreadsApp;
use serde_json::Value;

/// Write `<root>/<name>/Cargo.toml` + `<root>/<name>/src/lib.rs` with `body`.
fn make_crate(root: &Path, name: &str, body: &str) {
    let dir = root.join(name);
    fs::create_dir_all(dir.join("src")).unwrap();
    fs::write(
        dir.join("Cargo.toml"),
        format!("[package]\nname = \"{name}\"\nversion = \"0.0.0\"\nedition = \"2021\"\n"),
    )
    .unwrap();
    fs::write(dir.join("src/lib.rs"), body).unwrap();
}

#[test]
fn knowledge_scan_counts_known_repo() {
    let tmp = tempfile::tempdir().unwrap();
    let ws_root = tmp.path();

    // ── repo "alpha": 1 struct + 3 fns, and exactly 2 call expressions. ──
    // symbols: struct Foo, fn one, fn two, fn helper       → 4 items.
    // calls:   two() and helper() inside one()             → 2 call edges.
    make_crate(
        ws_root,
        "alpha",
        r#"
pub struct Foo { pub n: u32 }
pub fn one() {
    two();
    helper();
}
pub fn two() {}
fn helper() {}
"#,
    );
    // ── repo "beta": 1 fn, 1 call. ──
    make_crate(
        ws_root,
        "beta",
        r#"
pub fn solo() {
    work();
}
fn work() {}
"#,
    );

    // Warehouse root is irrelevant for the Knowledge tab (local, empty); the
    // Knowledge scan reads `workspace_root/<repo>` off disk.
    let wh = tmp.path().join("warehouse");
    fs::create_dir_all(&wh).unwrap();
    let mut app = UrdrThreadsApp::with_repos(
        wh,
        "testws".to_string(),
        ws_root.to_path_buf(),
        vec!["alpha".to_string(), "beta".to_string()],
    );

    // Run the scan synchronously (production runs it on a background thread).
    app.knowledge_scan_blocking_for_test();

    let kn = &app.state_json()["knowledge"];
    eprintln!("knowledge state = {}", serde_json::to_string_pretty(kn).unwrap());

    assert_eq!(kn["scanned_repos"], 2, "both repos scanned");
    assert_eq!(kn["scanning"], Value::Bool(false), "scan finished (not in flight)");

    // Per-repo totals — exact, from source we wrote.
    let totals = kn["repo_totals"].as_array().unwrap();
    let alpha = totals.iter().find(|r| r["repo"] == "alpha").expect("alpha row");
    assert_eq!(alpha["ok"], Value::Bool(true));
    assert_eq!(alpha["symbols"], 4, "alpha: struct Foo + fn one + fn two + fn helper");
    assert_eq!(alpha["calls"], 2, "alpha: two() + helper() inside one()");

    let beta = totals.iter().find(|r| r["repo"] == "beta").expect("beta row");
    assert_eq!(beta["symbols"], 2, "beta: fn solo + fn work");
    assert_eq!(beta["calls"], 1, "beta: work() inside solo()");

    // LAW #6: the rendered rows (one per crate) are in `state_json`.
    let rows = kn["rows"].as_array().unwrap();
    assert_eq!(kn["crates"], 2, "two crates → two bubble rows");
    assert_eq!(rows.len(), 2);
    let alpha_row = rows.iter().find(|r| r["krate"] == "alpha").expect("alpha bubble");
    assert_eq!(alpha_row["symbols"], 4);
    assert_eq!(alpha_row["calls"], 2);
}

#[test]
fn knowledge_switch_workspace_resets_and_rescopes() {
    let tmp = tempfile::tempdir().unwrap();
    let ws_root = tmp.path();
    make_crate(ws_root, "alpha", "pub fn a() {}\n");
    make_crate(ws_root, "gamma", "pub fn g() {} pub struct G;\n");

    let wh = tmp.path().join("warehouse");
    fs::create_dir_all(&wh).unwrap();
    let mut app = UrdrThreadsApp::with_repos(
        wh,
        "testws".to_string(),
        ws_root.to_path_buf(),
        vec!["alpha".to_string()],
    );

    // Scan the first workspace (just "alpha").
    app.knowledge_scan_blocking_for_test();
    let kn = &app.state_json()["knowledge"];
    assert_eq!(kn["scanned_repos"], 1, "first workspace: 1 repo");
    assert_eq!(kn["configured_repos"], serde_json::json!(["alpha"]));

    // Switch workspace → re-scope to a *different* repo set ("gamma").
    app.knowledge_set_workspace_for_test(ws_root.to_path_buf(), vec!["gamma".to_string()]);
    let kn = &app.state_json()["knowledge"];
    // Immediately after the switch the stale scans are dropped (reset)…
    assert_eq!(kn["scanned_repos"], 0, "switch drops stale scans");
    assert_eq!(kn["crates"], 0, "no rows until the rescan runs");
    assert_eq!(kn["configured_repos"], serde_json::json!(["gamma"]), "re-scoped to new repos");

    // …and the rescan of the new scope produces the new repo's data.
    app.knowledge_scan_blocking_for_test();
    let kn = &app.state_json()["knowledge"];
    assert_eq!(kn["scanned_repos"], 1);
    let totals = kn["repo_totals"].as_array().unwrap();
    let gamma = totals.iter().find(|r| r["repo"] == "gamma").expect("gamma row");
    assert_eq!(gamma["symbols"], 2, "gamma: fn g + struct G");
    // The old repo "alpha" is gone from the rescoped scan.
    assert!(totals.iter().all(|r| r["repo"] != "alpha"), "alpha no longer scanned");
}