Files
notescraft/apps/notescraft-cli/tests/control.rs
T
2026-10-14 23:59:28 -03:00

95 lines
4.3 KiB
Rust

//! The CLI and the control channel end to end: a write subcommand defers to
//! the running app's live session (SPEC 10.1, 4.2, D9) even though the vault
//! lock belongs to that app, and falls back to the local path when there is no
//! app.
use notescraft_engine::{CommandResult, CommandSpec, ControlServer, ParamKind, ParamSpec, Registry, Session, register_core};
use serde_json::{Value, json};
mod common;
use common::{empty_vault, read_file, run, run_with_stdin};
/// The `test.echo` body: params back out unchanged.
#[expect(clippy::unnecessary_wraps, reason = "the registry signature demands CommandResult")]
fn echo(_session: &mut Session, params: &Value) -> CommandResult {
Ok(params.clone())
}
/// A vault (its content untouched) with the app's stand-in running: a control
/// server over a live session and a pumper thread in place of the UI thread.
///
/// Returns the vault root; `stop` ends the pumper thread.
fn spun_up(name: &str) -> (std::path::PathBuf, std::sync::mpsc::Sender<()>) {
let root = empty_vault(name);
let session = Session::open(&root).expect("open the vault");
let mut registry = Registry::new();
register_core(&mut registry).expect("register the core commands");
registry
.register(CommandSpec::new("test.echo", "Echo", "echoes its params", |_: &Session| true, echo).with_param(ParamSpec::optional(
"question",
ParamKind::Integer,
"number echoed back",
)))
.expect("register test.echo");
let handle = ControlServer::start_with(session.vault(), notescraft_engine::ControlOptions::default()).expect("start the control server");
let (stop_tx, stop_rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let mut session = session;
let mut handle = handle;
while stop_rx.try_recv().is_err() {
if handle.pump(&mut session, &registry) == 0 {
std::thread::sleep(std::time::Duration::from_millis(1));
}
}
});
(root, stop_tx)
}
/// A write goes to the live app even though the app owns the vault lock: the
/// CLI never takes the lock itself. `run` defers to the app's registry too.
#[test]
fn a_write_defers_to_the_running_app() {
let (root, stop) = spun_up("remote-write");
let out = run_with_stdin(&root, &["--json", "write", "a.md", "--yes"], "hello from the app\n");
assert_eq!(out.code, 0, "{}", out.stdout);
assert_eq!(out.envelope().get("ok"), Some(&json!(true)), "{}", out.stdout);
// The note exists only because the app session wrote it.
assert_eq!(read_file(&root, "a.md"), "hello from the app\n", "the app session wrote the note");
// `run` defers too: the stub command only exists in the app's registry.
let out = run(&root, &["--json", "run", "test.echo", r#"{"question":1}"#]);
let _ = stop.send(());
assert_eq!(out.code, 0, "{}", out.stdout);
assert_eq!(out.envelope().pointer("/result/question"), Some(&json!(1u64)), "{}", out.stdout);
}
/// Without a running app the CLI falls back to the local path and answers.
#[test]
fn without_an_app_the_write_falls_back_to_the_local_path() {
let root = empty_vault("local-write");
let out = run_with_stdin(&root, &["--json", "write", "a.md", "--yes"], "hello local\n");
assert_eq!(out.code, 0, "{}", out.stdout);
assert_eq!(read_file(&root, "a.md"), "hello local\n", "the local session wrote the note");
}
/// `control ping` without an app is a command error with a stable code, never
/// a usage error and never a panic.
#[test]
fn control_ping_without_an_app_reports_the_control_error() {
let root = empty_vault("ping-offline");
let out = run(&root, &["--json", "control", "ping"]);
assert_eq!(out.code, 1, "{}", out.stdout);
assert_eq!(out.envelope().get("ok"), Some(&json!(false)), "{}", out.stdout);
assert_eq!(out.envelope().pointer("/error/code"), Some(&json!("control")), "{}", out.stdout);
}
/// `control ping` against the live app round trips.
#[test]
fn control_ping_reaches_the_running_app() {
let (root, stop) = spun_up("ping");
let out = run(&root, &["--json", "control", "ping"]);
let _ = stop.send(());
assert_eq!(out.code, 0, "{}", out.stdout);
assert_eq!(out.envelope().get("ok"), Some(&json!(true)), "{}", out.stdout);
assert_eq!(out.envelope().pointer("/result/protocol"), Some(&json!(1u64)), "{}", out.stdout);
}