Files
Marcos Paulo cac6b0ed39
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
feat: registry consumers (grammar, theme, pack), seed reader, wasm plugin host with approval, wanted scan, STIX and Lottie store lookups, plugin auto-disable persistence
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 18:35:47 -03:00

172 lines
7.4 KiB
Rust

//! `grammar.install`, `grammar.remove` and `grammar.info`, with a fake transport (no network).
mod common;
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use common::{temp_config, temp_vault};
use notescraft_engine::{AppSettings, Registry, Session, register_core};
use notescraft_syntax::Error as SyntaxError;
use notescraft_syntax::fetch::Transport;
use notescraft_syntax::validate::sha256_hex;
use serde_json::{Value, json};
/// The highlighter's grammar list is process-wide, so these tests take turns.
static TURN: Mutex<()> = Mutex::new(());
fn turn() -> std::sync::MutexGuard<'static, ()> {
TURN.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
}
const TINY: &str = "%YAML 1.2\n---\nname: Tiny\nfile_extensions: [tiny]\nscope: source.tiny\ncontexts:\n main:\n - match: '\\bfoo\\b'\n scope: keyword.control.tiny\n";
const RAW: &str = "https://git.marcospaulo.dev.br/masi/notescraft-registry/raw/branch/main/";
/// Serves canned bodies by URL and records every request.
#[derive(Debug, Clone, Default)]
struct Fake {
files: Arc<Mutex<BTreeMap<String, Vec<u8>>>>,
asked: Arc<Mutex<Vec<String>>>,
}
impl Transport for Fake {
fn get(&self, url: &str, _max: usize) -> notescraft_syntax::Result<Vec<u8>> {
self.asked.lock().expect("lock").push(url.to_owned());
self.files.lock().expect("lock").get(url).cloned().ok_or(SyntaxError::Offline)
}
}
fn index(sha: &str) -> String {
json!({ "version": 1, "grammars": [
{ "id": "tiny", "name": "Tiny", "format": "sublime-syntax", "file": "grammars/tiny.sublime-syntax", "sha256": sha, "license": "MIT", "sample": "foo\n" },
{ "id": "plist", "format": "tmLanguage", "file": "grammars/plist.tmLanguage", "sha256": "00" },
{ "id": "evil", "format": "sublime-syntax", "file": "../../etc/passwd", "sha256": "00" }
] })
.to_string()
}
struct Setup {
registry: Registry<Session>,
session: Session,
fake: Fake,
config: common::TempVault,
_vault: common::TempVault,
}
fn setup(name: &str, body: &str) -> Setup {
let vault = temp_vault(name);
let config = temp_config(name);
let mut registry = Registry::new();
register_core(&mut registry).expect("register");
let mut session = Session::open(vault.path()).expect("open");
session.use_settings_dir(config.path());
let fake = Fake::default();
{
let mut files = fake.files.lock().expect("lock");
files.insert(format!("{RAW}index.json"), index(&sha256_hex(body.as_bytes())).into_bytes());
files.insert(format!("{RAW}grammars/tiny.sublime-syntax"), body.as_bytes().to_vec());
}
session.set_grammar_transport(Box::new(fake.clone()));
Setup { registry, session, fake, config, _vault: vault }
}
fn run(s: &mut Setup, id: &str, params: &Value) -> Result<Value, notescraft_engine::Error> {
s.registry.run(id, &mut s.session, params)
}
fn installed(s: &Setup) -> AppSettings {
notescraft_engine::load_at(s.config.path()).0
}
#[test]
fn a_registry_install_writes_the_file_and_records_it() {
let _turn = turn();
let mut s = setup("gram-ok", TINY);
let result = run(&mut s, "grammar.install", &json!({ "id": "tiny" })).expect("installs");
assert_eq!(result["name"], json!("Tiny"));
assert_eq!(result["license"], json!("MIT"));
let file = s.config.path().join("grammars/tiny.sublime-syntax");
assert_eq!(std::fs::read_to_string(&file).expect("file"), TINY);
assert_eq!(installed(&s).grammars.iter().find(|g| g.id == "tiny").map(|g| g.name.as_str()), Some("Tiny"));
assert!(notescraft_syntax::load::is_known("tiny"), "the installed grammar is live in the highlighter");
let info = run(&mut s, "grammar.info", &json!({ "label": "pyhton" })).expect("info");
assert_eq!(info["installed"].as_array().map(Vec::len), Some(1));
assert_eq!(info["installed"][0]["source"], json!("legacy"));
assert_eq!(info["label"]["suggestion"], json!("python"));
assert_eq!(info["label"]["bundled"], json!(false));
}
#[test]
fn a_pasted_link_installs_without_a_checksum() {
let _turn = turn();
let mut s = setup("gram-url", TINY);
s.fake.files.lock().expect("lock").insert("https://example.org/my.sublime-syntax".to_owned(), TINY.as_bytes().to_vec());
run(&mut s, "grammar.install", &json!({ "id": "mine", "url": "https://example.org/my.sublime-syntax" })).expect("installs");
assert!(s.config.path().join("grammars/mine.sublime-syntax").exists());
}
#[test]
fn every_failed_gate_saves_nothing() {
let _turn = turn();
let mut s = setup("gram-bad", TINY);
let before = installed(&s);
for params in [
json!({ "id": "plist" }),
json!({ "id": "evil" }),
json!({ "id": "absent" }),
json!({ "id": "Bad Id" }),
json!({ "id": "x", "url": "http://example.org/a.sublime-syntax" }),
json!({ "id": "x", "url": "https://127.0.0.1/a.sublime-syntax" }),
json!({ "id": "x", "url": "https://example.org/offline.sublime-syntax" }),
] {
assert!(run(&mut s, "grammar.install", &params).is_err(), "{params}");
}
s.fake.files.lock().expect("lock").insert(format!("{RAW}grammars/tiny.sublime-syntax"), b"tampered".to_vec());
assert!(run(&mut s, "grammar.install", &json!({ "id": "tiny" })).is_err(), "checksum mismatch");
assert_eq!(installed(&s), before);
assert!(!s.config.path().join("grammars").exists() || std::fs::read_dir(s.config.path().join("grammars")).expect("dir").next().is_none());
}
#[test]
fn remove_deletes_the_file_and_the_entry() {
let _turn = turn();
let mut s = setup("gram-remove", TINY);
run(&mut s, "grammar.install", &json!({ "id": "tiny" })).expect("installs");
run(&mut s, "grammar.remove", &json!({ "id": "tiny" })).expect("removes");
assert!(!s.config.path().join("grammars/tiny.sublime-syntax").exists());
assert_eq!(installed(&s).grammars, Vec::new());
assert!(!notescraft_syntax::load::is_known("tiny"), "removal unloads it");
assert!(run(&mut s, "grammar.remove", &json!({ "id": "tiny" })).is_err(), "second removal is an error");
}
#[test]
fn bad_entries_are_dropped_and_the_rest_sorted() {
let _turn = turn();
let entry = |id: &str, file: &str| notescraft_engine::GrammarInfo { id: id.into(), file: file.into(), ..Default::default() };
let mut settings = AppSettings {
grammars: vec![
entry("zed", "zed.sublime-syntax"),
entry("ok", "ok.sublime-syntax"),
entry("../x", "../x.sublime-syntax"),
entry("mismatch", "other.sublime-syntax"),
entry("ok", "ok.sublime-syntax"),
],
..AppSettings::default()
};
let notes = settings.validate();
assert_eq!(settings.grammars.iter().map(|g| g.id.as_str()).collect::<Vec<_>>(), ["ok", "zed"]);
assert!(notes.iter().any(|n| n.contains("dropped 3")), "{notes:?}");
}
#[test]
fn install_text_checks_the_grammar_then_saves_it() {
let _turn = turn();
let mut s = setup("gram-text", TINY);
run(&mut s, "grammar.install_text", &json!({ "id": "tiny", "text": TINY, "license": "MIT" })).expect("installs");
assert!(s.config.path().join("grammars/tiny.sublime-syntax").exists());
assert!(notescraft_syntax::load::is_known("tiny"));
assert!(run(&mut s, "grammar.install_text", &json!({ "id": "bad", "text": "nope: [" })).is_err());
assert!(run(&mut s, "grammar.install_text", &json!({ "id": "bad", "text": "<?xml version=\"1.0\"?><plist/>" })).is_err());
assert!(!s.config.path().join("grammars/bad.sublime-syntax").exists());
}