Files
notescraft/crates/mcp/tests/knowledge_tools.rs
Marcos Paulo 15d7f5e873 feat: editor blocks and lint, vault knowledge, calendar plugin, macOS menu
- notescraft-lint (rules, safe fixes, learned style) and notescraft-blocks (block model and edits); engine lint.* and block.* commands with CLI and MCP parity
- Lint tab, underlines, slash menu with preview in Source and Visual, block grip and menu, Settings > Editor and lint, Vim block keys
- Visual view: tables, note embeds, %% comments, wrapping property chips
- Vault knowledge (Related tab, assistant sources, Settings) and calendar plugin
- Sidebar top padding fixed, dot grid follows PaperCraft spacing, right-card tabs no longer cover the collapse button

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-18 12:36:00 -03:00

104 lines
5.0 KiB
Rust

//! The knowledge tools over MCP: `vault_context` for the assistant, every
//! `knowledge_*` command by name, the per-plugin toggle and the exclusions.
mod common;
use common::{ask, call_tool, is_error, registry, seed, temp_vault};
use notescraft_engine::{AppSettings, Session, SessionConfig};
use notescraft_mcp::{Server, ServerOptions};
use serde_json::{Value, json};
const TRIP: &str = "# Trip\n\nBook the flight to Lisbon and reserve the hotel near the river.\n";
const SECRET: &str = "# Diary\n\nThe lisbon flight password is hunter2 and the river hotel is secret.\n";
const WRITABLE: ServerOptions = ServerOptions { write: true, allow_delete: false, via_control: false };
fn config() -> SessionConfig {
let mut config = SessionConfig::default();
config.knowledge.excluded = vec!["Private".to_owned()];
config
}
fn fixture(name: &str, options: ServerOptions, settings: Option<AppSettings>) -> (common::TempVault, Server) {
let vault = temp_vault(name);
seed(vault.path(), &[("Trips/lisbon.md", TRIP), ("Private/diary.md", SECRET)]);
let mut session = Session::open_with(vault.path(), config()).expect("open");
if let Some(settings) = settings {
session.apply_app_settings(&settings).expect("settings");
}
(vault, Server::new(session, registry(), options))
}
fn names(server: &mut Server) -> Vec<String> {
let reply = ask(server, &json!({"jsonrpc": "2.0", "id": 1, "method": "tools/list"}).to_string());
reply["result"]["tools"].as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap().to_owned()).collect()
}
/// The engine's own answer over a twin vault (the server holds the first one's lock).
fn engine(id: &str, params: &Value) -> Value {
let twin = temp_vault("kn-mcp-twin");
seed(twin.path(), &[("Trips/lisbon.md", TRIP), ("Private/diary.md", SECRET)]);
let mut session = Session::open_with(twin.path(), config()).expect("session");
registry().run(id, &mut session, params).expect("engine run")
}
#[test]
fn vault_context_equals_the_engine_and_search_has_the_engine_shape() {
let (_vault, mut server) = fixture("kn-mcp-parity", WRITABLE, None);
let query = json!({ "query": "lisbon flight hotel" });
let via_alias = call_tool(&mut server, "vault_context", &query);
assert!(via_alias.get("error").is_none(), "{via_alias}");
assert_eq!(via_alias, call_tool(&mut server, "knowledge_context", &query));
assert_eq!(via_alias, engine("knowledge.context", &query));
assert_eq!(via_alias["passages"][0]["path"], "Trips/lisbon.md", "{via_alias}");
let search = call_tool(&mut server, "knowledge_search", &query);
let direct = engine("knowledge.search", &query);
assert_eq!(search["passages"][0]["path"], direct["passages"][0]["path"]);
let keys = |value: &Value| value["passages"][0].as_object().map(|o| o.keys().cloned().collect::<Vec<_>>());
assert_eq!(keys(&search), keys(&direct));
}
#[test]
fn excluded_folders_never_appear() {
let (_vault, mut server) = fixture("kn-mcp-excluded", WRITABLE, None);
for tool in ["vault_context", "knowledge_search", "knowledge_context"] {
let out = call_tool(&mut server, tool, &json!({ "query": "password hunter2 secret" }));
assert!(!out.to_string().contains("hunter2") && !out.to_string().contains("Private/"), "{tool}: {out}");
}
}
#[test]
fn the_assistant_list_holds_vault_context_and_only_when_enabled() {
let options = ServerOptions { via_control: true, ..WRITABLE };
let (_vault, mut server) = fixture("kn-mcp-list", options, None);
let listed = names(&mut server);
assert!(listed.iter().any(|name| name == "vault_context"), "{listed:?}");
assert!(!listed.iter().any(|name| name.starts_with("knowledge_")), "{listed:?}");
assert!(listed.len() <= 24, "{listed:?}");
let mut settings = AppSettings::default();
settings.assistant.plugin_tools.insert("knowledge".to_owned(), false);
let (_vault, mut off) = fixture("kn-mcp-off", options, Some(settings));
assert!(!names(&mut off).iter().any(|name| name == "vault_context"));
assert!(is_error(&mut off, "vault_context", &json!({ "query": "lisbon" })));
}
#[test]
fn a_plain_server_lists_every_knowledge_command_and_run_command_reaches_them() {
let (_vault, mut server) = fixture("kn-mcp-plain", WRITABLE, None);
let listed = names(&mut server);
for tool in ["knowledge_search", "knowledge_context", "knowledge_related", "knowledge_status", "knowledge_rebuild", "vault_context"] {
assert!(listed.iter().any(|name| name == tool), "{tool}");
}
let status = call_tool(&mut server, "run_command", &json!({ "id": "knowledge.status" }));
assert!(status.get("passages").is_some(), "{status}");
}
#[test]
fn a_read_only_server_answers_reads() {
let (_vault, mut server) = fixture("kn-mcp-ro", ServerOptions { write: false, ..WRITABLE }, None);
assert!(!is_error(&mut server, "vault_context", &json!({ "query": "lisbon" })));
assert!(!is_error(&mut server, "knowledge_search", &json!({ "query": "lisbon" })));
assert!(is_error(&mut server, "vault_context", &json!({})), "`query` is required");
}