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

nordisk/nornir

tests/cli_knowledge_query_server.rs

//! **`nornir knowledge query` asks the server; it never opens the lake.**
//!
//! The red (2026-10-16): `knowledge query` (and every verb built on
//! `code_room_read`) built a `CodeRoom` over the CLI box's own
//! `<builds>/mimir_v2` lake and answered from it — with `--server` set too. The
//! UI asks the server's `CodeRoom.Answer`; the CLI must ask the same door.
//!
//! * With `--server` (or `$NORNIR_SERVER`): the question goes to
//!   `CodeRoom.Answer` with the workspace header, a `queued` answer is asked
//!   again until it lands, and the printed JSON is the server's.
//! * Without one: the verb is REFUSED by name, and no lake is created.
//!
//! Drives the real `nornir` binary against an in-test `CodeRoom` server.
#![cfg(all(feature = "server", feature = "wire-client"))]

use std::path::Path;
use std::process::Command;
use std::sync::{Arc, Mutex};

use nornir_wire::pb;
use pb::code_room_server::{CodeRoom, CodeRoomServer};

const WS: &str = "kqws";

#[derive(Default, Clone)]
struct Fake {
    asked: Arc<Mutex<Vec<(String, serde_json::Value, String)>>>,
}

#[tonic::async_trait]
impl CodeRoom for Fake {
    async fn answer(&self, req: tonic::Request<pb::CodeRoomQuestion>) -> Result<tonic::Response<pb::JsonResponse>, tonic::Status> {
        let ws = req.metadata().get("nornir-workspace").and_then(|v| v.to_str().ok()).unwrap_or("").to_string();
        let q = req.into_inner();
        let params: serde_json::Value = serde_json::from_str(&q.params_json).unwrap_or_default();
        let n = {
            let mut asked = self.asked.lock().unwrap();
            asked.push((q.surface.clone(), params, ws));
            asked.len()
        };
        // the first ask is queued on the server's jera lane; the next one lands
        let body = if n == 1 {
            serde_json::json!({ "surface": q.surface, "state": "queued", "job_id": "job-1", "ms": 0, "note": "queued on jera — job job-1 (queued 0s); ask again for the answer" })
        } else {
            serde_json::json!({ "surface": q.surface, "state": "complete", "names": ["from_the_server"] })
        };
        Ok(tonic::Response::new(pb::JsonResponse { json: body.to_string() }))
    }
}

fn spawn_fake() -> (String, Fake, tokio::runtime::Runtime) {
    let rt = tokio::runtime::Builder::new_multi_thread().worker_threads(2).enable_all().build().unwrap();
    let fake = Fake::default();
    let svc = fake.clone();
    let addr = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap();
    rt.spawn(async move {
        tonic::transport::Server::builder().add_service(CodeRoomServer::new(svc)).serve(addr).await.unwrap();
    });
    for _ in 0..200 {
        if std::net::TcpStream::connect(addr).is_ok() {
            break;
        }
        std::thread::sleep(std::time::Duration::from_millis(10));
    }
    (format!("http://{addr}"), fake, rt)
}

fn lakes_under(dir: &Path) -> Vec<String> {
    let mut out = Vec::new();
    let mut stack = vec![dir.to_path_buf()];
    while let Some(d) = stack.pop() {
        for e in std::fs::read_dir(&d).into_iter().flatten().flatten() {
            let p = e.path();
            if p.is_dir() {
                if p.file_name().is_some_and(|f| f == "mimir_v2") {
                    out.push(p.display().to_string());
                }
                stack.push(p);
            }
        }
    }
    out
}

fn nornir(home: &Path, cwd: &Path) -> Command {
    let mut c = Command::new(env!("CARGO_BIN_EXE_nornir"));
    c.current_dir(cwd)
        .env("HOME", home)
        .env("NORNIR_HOME", home.join(".nornir"))
        .env_remove("NORNIR_SERVER")
        .env_remove("NORNIR_SERVER_TOKEN")
        .env_remove("NORNIR_WORKSPACE")
        .env_remove("NORNIR_CONFIG");
    c
}

#[test]
fn knowledge_query_asks_the_servers_code_room_door() {
    let (endpoint, fake, _rt) = spawn_fake();
    let home = tempfile::tempdir().unwrap();
    let out = nornir(home.path(), home.path())
        .args(["--server", &endpoint, "--workspace", WS, "knowledge", "query", "alpha", "callers", "charge"])
        .output()
        .expect("run nornir");
    let (stdout, stderr) = (String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr));
    assert!(out.status.success(), "stderr: {stderr}\nstdout: {stdout}");
    let asked = fake.asked.lock().unwrap().clone();
    assert_eq!(asked.len(), 2, "asked once, queued, asked again: {asked:?}");
    for (surface, params, ws) in &asked {
        assert_eq!((surface.as_str(), ws.as_str()), ("Knowledge.Callers", WS));
        assert_eq!((params["repo"].as_str(), params["arg"].as_str()), (Some("alpha"), Some("charge")), "{params}");
    }
    let printed: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap_or_else(|e| panic!("not JSON ({e}): {stdout}"));
    assert_eq!(printed["names"], serde_json::json!(["from_the_server"]), "{printed}");
    assert!(lakes_under(home.path()).is_empty(), "the CLI opened a lake: {:?}", lakes_under(home.path()));
}

#[test]
fn without_a_server_knowledge_query_is_refused_by_name_and_opens_no_lake() {
    let home = tempfile::tempdir().unwrap();
    let cfg = home.path().join(format!("workspace_{WS}/release/nornir.toml"));
    std::fs::create_dir_all(cfg.parent().unwrap()).unwrap();
    std::fs::write(&cfg, "[repo.alpha]\npath = \"alpha\"\n").unwrap();
    let out = nornir(home.path(), cfg.parent().unwrap())
        .args(["--config", cfg.to_str().unwrap(), "knowledge", "query", "alpha", "callers", "charge"])
        .output()
        .expect("run nornir");
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(!out.status.success(), "a local read must be refused: {stderr}");
    assert!(
        stderr.contains("knowledge query callers") && stderr.contains("never opens the code room's lake") && stderr.contains("--server"),
        "refused by name, with the way in: {stderr}"
    );
    assert!(lakes_under(home.path()).is_empty(), "the CLI opened a lake: {:?}", lakes_under(home.path()));
}