Files
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

76 lines
3.5 KiB
Rust

//! The lint tools over MCP: `lint_check` and `lint_fix_all_safe` equal the engine, the rest is reachable by name,
//! and a read-only server answers reads only.
mod common;
use common::{ask, call_tool, is_error, registry, seed, temp_vault};
use notescraft_engine::Session;
use notescraft_mcp::{Server, ServerOptions};
use serde_json::{Value, json};
const MESSY: &str = "# A\n\n### B\n\ntext with gaps\n";
const WRITABLE: ServerOptions = ServerOptions { write: true, allow_delete: false, via_control: false };
fn fixture(name: &str, options: ServerOptions) -> (common::TempVault, Server) {
let vault = temp_vault(name);
seed(vault.path(), &[("messy.md", MESSY)]);
let session = Session::open(vault.path()).expect("open");
(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("lint-mcp-twin");
seed(twin.path(), &[("messy.md", MESSY)]);
let mut session = Session::open(twin.path()).expect("session");
registry().run(id, &mut session, params).expect("engine run")
}
#[test]
fn lint_check_and_fix_all_safe_equal_the_engine() {
let (vault, mut server) = fixture("lint-mcp-parity", WRITABLE);
let params = json!({ "path": "messy.md" });
let checked = call_tool(&mut server, "lint_check", &params);
assert!(checked.get("error").is_none(), "{checked}");
assert_eq!(checked, engine("lint.check", &params));
assert_eq!(checked, call_tool(&mut server, "run_command", &json!({ "id": "lint.check", "params": params })));
let fixed = call_tool(&mut server, "lint_fix_all_safe", &params);
assert_eq!(fixed, engine("lint.fix_all_safe", &params));
assert_eq!(std::fs::read_to_string(vault.path().join("messy.md")).unwrap(), "# A\n\n## B\n\ntext with gaps\n");
}
#[test]
fn the_assistant_list_holds_only_the_two_lint_aliases() {
let (_vault, mut server) = fixture("lint-mcp-list", ServerOptions { via_control: true, ..WRITABLE });
let listed = names(&mut server);
for tool in ["lint_check", "lint_fix_all_safe"] {
assert!(listed.iter().any(|name| name == tool), "{tool}: {listed:?}");
}
assert!(!listed.iter().any(|name| name.starts_with("lint_") && !["lint_check", "lint_fix_all_safe"].contains(&name.as_str())), "{listed:?}");
}
#[test]
fn a_plain_server_lists_every_lint_command() {
let (_vault, mut server) = fixture("lint-mcp-plain", WRITABLE);
let listed = names(&mut server);
for tool in ["lint_check", "lint_fix", "lint_fix_all_safe", "lint_rules", "lint_dismiss", "lint_state", "lint_state_clear", "lint_style"] {
assert!(listed.iter().any(|name| name == tool), "{tool}");
}
}
#[test]
fn a_read_only_server_checks_but_never_fixes() {
let (_vault, mut server) = fixture("lint-mcp-ro", ServerOptions { write: false, ..WRITABLE });
assert!(!is_error(&mut server, "lint_check", &json!({ "path": "messy.md" })));
assert!(!is_error(&mut server, "lint_rules", &json!({})));
assert!(is_error(&mut server, "lint_fix_all_safe", &json!({ "path": "messy.md" })));
assert!(is_error(&mut server, "lint_check", &json!({ "path": "../x.md" })));
assert!(is_error(&mut server, "lint_check", &json!({})), "`path` is required");
}