Files
2026-10-15 02:00:16 -03:00

136 lines
5.1 KiB
Rust

//! Fixtures: throw-away vaults and servers over them.
//!
//! Every test binary compiles this module, so a helper used by one file is
//! dead code in the others.
#![allow(dead_code)]
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use notescraft_engine::{Registry, Session, register_core};
use notescraft_mcp::{Server, ServerOptions};
/// Counter making two vaults of the same test different.
static COUNTER: AtomicU64 = AtomicU64::new(0);
/// A vault folder that deletes itself when it goes out of scope.
pub struct TempVault {
path: PathBuf,
}
impl TempVault {
/// The vault root to hand to [`Session::open`].
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
}
impl Drop for TempVault {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.path);
}
}
/// An empty vault folder named after the test and the process.
#[must_use]
pub fn temp_vault(name: &str) -> TempVault {
let unique = COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!("notescraft-mcp-{}-{name}-{unique}", std::process::id()));
let _ = std::fs::remove_dir_all(&path);
std::fs::create_dir_all(&path).expect("create the temp vault");
TempVault { path }
}
/// Writes `(path, content)` pairs into `vault`, creating the folders.
pub fn seed(vault: &Path, files: &[(&str, &str)]) {
for (path, content) in files {
let target = vault.join(path);
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent).expect("create the temp folder");
}
std::fs::write(&target, content).expect("write the fixture file");
}
}
/// The fixtures every server test starts from.
#[must_use]
pub fn seeded_vault(name: &str) -> TempVault {
let vault = temp_vault(name);
seed(
vault.path(),
&[
("Alpha.md", "---\ntags: [work]\n---\n# Alpha\n\nlinks [[Beta]] and #work/project\n"),
("Beta.md", "# Beta\n\nmentions the plan\n"),
("folder/Gamma.md", "# Gamma\n\n[[Beta]]\n"),
("img.png", "not a note"),
],
);
vault
}
/// The core registry every fixture uses.
#[must_use]
pub fn registry() -> Registry<Session> {
let mut registry = Registry::new();
register_core(&mut registry).expect("register the core commands");
registry
}
/// A writable server over a fresh vault seeded with the fixtures.
pub fn writable_server(name: &str) -> (TempVault, Server) {
let vault = seeded_vault(name);
let session = Session::open(vault.path()).expect("open the vault");
(vault, Server::new(session, registry(), ServerOptions { write: true, allow_delete: false, via_control: false }))
}
/// A writable server over a vault seeded with the fixtures plus `extra` notes
/// named `extra-00000.md`, so a listing has more than one page.
pub fn server_with_extra(name: &str, extra: usize) -> (TempVault, Server) {
let vault = seeded_vault(name);
let files: Vec<(String, String)> = (0..extra).map(|index| (format!("extra-{index:05}.md"), format!("# extra {index}\n"))).collect();
let borrowed: Vec<(&str, &str)> = files.iter().map(|(path, body)| (path.as_str(), body.as_str())).collect();
seed(vault.path(), &borrowed);
let session = Session::open(vault.path()).expect("open the vault");
(vault, Server::new(session, registry(), ServerOptions { write: true, allow_delete: false, via_control: false }))
}
/// A server over a read-only session.
pub fn read_only_server(name: &str) -> (TempVault, Server) {
let vault = seeded_vault(name);
let session = Session::open_read_only(vault.path()).expect("open the vault read-only");
(vault, Server::new(session, registry(), ServerOptions { write: false, allow_delete: false, via_control: false }))
}
/// Sends one JSON-RPC line and returns the reply, failing the test when none came.
pub fn ask(server: &mut Server, line: &str) -> serde_json::Value {
let reply = server.handle_line(line).expect("a reply is due");
serde_json::from_str(&reply).unwrap_or_else(|error| panic!("the reply is JSON: {error}; {reply}"))
}
/// Calls one tool by name and returns the `structuredContent` of the result.
pub fn call_tool(server: &mut Server, name: &str, arguments: &serde_json::Value) -> serde_json::Value {
let request = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { "name": name, "arguments": arguments },
});
let reply = ask(server, &request.to_string());
let result = reply.get("result").unwrap_or_else(|| panic!("the call succeeded at the protocol level: {reply}"));
result.get("structuredContent").cloned().unwrap_or(serde_json::Value::Null)
}
/// Whether a tool call came back as a tool error.
pub fn is_error(server: &mut Server, name: &str, arguments: &serde_json::Value) -> bool {
let request = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { "name": name, "arguments": arguments },
});
let reply = ask(server, &request.to_string());
reply.get("result").and_then(|result| result.get("isError")).and_then(serde_json::Value::as_bool).unwrap_or(false)
}