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

208 lines
9.2 KiB
Rust

//! The grammar consumer: grammars from the add-on store (single entries and packs) reach the
//! highlighter next to the pre-registry ones, and `grammar.*` use the shared installer.
mod addons_support;
mod common;
use std::sync::{Arc, Mutex};
use addons_support::{Shared, TINY, entry, index_json, serve};
use common::{TempVault, temp_config, temp_vault};
use notescraft_engine::addons::{FakeTransport, FakeVerifier, INDEX_URL, Response};
use notescraft_engine::{Error, Registry, Session, register_core};
use notescraft_syntax::load;
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 OTHER: &str = "%YAML 1.2\n---\nname: Other\nfile_extensions: [other]\nscope: source.other\ncontexts:\n main:\n - match: '\\bbar\\b'\n scope: keyword.control.other\n";
/// `(kind, id, [(path, bytes)])`, all at version 1.0.0.
type Served<'a> = (&'a str, &'a str, Vec<(&'a str, &'a [u8])>);
struct Fixture {
reg: Registry<Session>,
session: Session,
config: std::path::PathBuf,
_guards: (TempVault, TempVault),
}
/// A session whose index lists `entries` and whose transport serves `files`
/// (`(kind, id, [(path, bytes)])`, all at version 1.0.0).
fn fixture(name: &str, entries: &[Value], files: &[Served], verify: bool) -> Fixture {
let (vault, config) = (temp_vault(name), temp_config(name));
let mut session = Session::open(vault.path()).unwrap();
session.use_settings_dir(config.path());
let doc = index_json(entries, &[]);
let mut net = FakeTransport::new().with_response(INDEX_URL, Ok(Response { status: 200, etag: None, bytes: doc }));
for (kind, id, list) in files {
net = serve(net, kind, id, "1.0.0", list);
}
session.set_addons_transport(Box::new(Shared(Arc::new(net))));
if verify {
session.set_addons_verifier(Box::new(FakeVerifier { accept: true }));
}
let mut reg = Registry::new();
register_core(&mut reg).unwrap();
let config_path = config.path().to_path_buf();
Fixture { reg, session, config: config_path, _guards: (vault, config) }
}
impl Fixture {
fn run(&mut self, id: &str, params: &Value) -> Result<Value, Error> {
self.reg.run(id, &mut self.session, params)
}
fn ok(&mut self, id: &str, params: &Value) -> Value {
self.run(id, params).unwrap_or_else(|e| panic!("{id}: {e}"))
}
fn code(&mut self, id: &str, params: &Value) -> String {
match self.run(id, params) {
Err(Error::Command { source, .. }) => source.code,
other => panic!("{id}: expected a command error, got {other:?}"),
}
}
fn installed(&mut self) -> Vec<(String, String)> {
let info = self.ok("grammar.info", &json!({}));
info["installed"].as_array().unwrap().iter().map(|g| (g["id"].as_str().unwrap().to_owned(), g["source"].as_str().unwrap().to_owned())).collect()
}
}
/// A grammar transport with no network.
#[derive(Debug)]
struct Offline;
impl notescraft_syntax::fetch::Transport for Offline {
fn get(&self, _url: &str, _max: usize) -> notescraft_syntax::Result<Vec<u8>> {
Err(notescraft_syntax::Error::Offline)
}
}
fn tiny_entry() -> Value {
entry("grammar", "tiny", "1.0.0", &[("tiny.sublime-syntax", TINY.as_bytes())], &[])
}
#[test]
fn a_registry_install_shows_in_grammar_info_and_reaches_the_highlighter() {
let _turn = turn();
let mut f = fixture("gc-info", &[tiny_entry()], &[("grammar", "tiny", vec![("tiny.sublime-syntax", TINY.as_bytes())])], true);
f.ok("registry.check", &json!({}));
f.ok("registry.install", &json!({ "kind": "grammar", "id": "tiny" }));
f.session.load_grammars();
assert!(load::is_known("tiny"));
assert_eq!(f.installed(), [("tiny".to_owned(), "registry".to_owned())]);
let label = f.ok("grammar.info", &json!({ "label": "tiny" }));
assert_eq!((label["label"]["installed"].clone(), label["label"]["bundled"].clone()), (json!(true), json!(false)));
f.ok("registry.remove", &json!({ "kind": "grammar", "id": "tiny" }));
f.session.load_grammars();
assert!(!load::is_known("tiny"), "removal returns the plain-text fallback");
}
#[test]
fn legacy_grammars_show_as_legacy_and_the_store_wins_a_repeated_id() {
let _turn = turn();
let mut f = fixture("gc-legacy", &[tiny_entry()], &[("grammar", "tiny", vec![("tiny.sublime-syntax", TINY.as_bytes())])], true);
f.ok("grammar.install_text", &json!({ "id": "tiny", "text": TINY }));
f.ok("grammar.install_text", &json!({ "id": "other", "text": OTHER }));
let mut legacy = f.installed();
legacy.sort();
assert_eq!(legacy, [("other".to_owned(), "legacy".to_owned()), ("tiny".to_owned(), "legacy".to_owned())]);
f.ok("registry.check", &json!({}));
f.ok("registry.install", &json!({ "kind": "grammar", "id": "tiny" }));
f.session.load_grammars();
assert!(load::is_known("tiny") && load::is_known("other"));
let sources = f.installed();
assert!(sources.contains(&("tiny".to_owned(), "registry".to_owned())) && sources.contains(&("other".to_owned(), "legacy".to_owned())), "{sources:?}");
assert!(f.config.join("grammars/tiny.sublime-syntax").exists(), "legacy files are never deleted by adoption");
}
#[test]
fn many_installs_in_one_frame_rebuild_the_highlighter_once() {
let _turn = turn();
let second = entry("grammar", "other", "1.0.0", &[("other.sublime-syntax", OTHER.as_bytes())], &[]);
let mut f = fixture(
"gc-once",
&[tiny_entry(), second],
&[("grammar", "tiny", vec![("tiny.sublime-syntax", TINY.as_bytes())]), ("grammar", "other", vec![("other.sublime-syntax", OTHER.as_bytes())])],
true,
);
f.ok("registry.check", &json!({}));
f.session.load_grammars();
let before = load::current().generation;
f.ok("registry.install", &json!({ "kind": "grammar", "id": "tiny" }));
f.ok("registry.install", &json!({ "kind": "grammar", "id": "other" }));
assert_eq!(load::current().generation, before, "the commands themselves do not rebuild");
f.session.load_grammars();
f.session.load_grammars();
assert_eq!(load::current().generation, before + 1);
}
#[test]
fn a_corrupt_store_grammar_is_skipped_and_the_rest_load() {
let _turn = turn();
let second = entry("grammar", "other", "1.0.0", &[("other.sublime-syntax", OTHER.as_bytes())], &[]);
let mut f = fixture(
"gc-corrupt",
&[tiny_entry(), second],
&[("grammar", "tiny", vec![("tiny.sublime-syntax", TINY.as_bytes())]), ("grammar", "other", vec![("other.sublime-syntax", OTHER.as_bytes())])],
true,
);
f.ok("registry.check", &json!({}));
f.ok("registry.install", &json!({ "kind": "grammar", "id": "tiny" }));
f.ok("registry.install", &json!({ "kind": "grammar", "id": "other" }));
let file = f.config.join("registry/store/grammar/tiny/1.0.0/tiny.sublime-syntax");
std::fs::write(&file, "not a grammar: [").unwrap();
f.session.load_grammars();
assert!(load::is_known("other") && !load::is_known("tiny"));
}
#[test]
fn grammar_install_uses_the_registry_when_the_id_is_indexed_and_grammar_remove_undoes_it() {
let _turn = turn();
let mut f = fixture("gc-cmd", &[tiny_entry()], &[("grammar", "tiny", vec![("tiny.sublime-syntax", TINY.as_bytes())])], true);
f.ok("registry.check", &json!({}));
let done = f.ok("grammar.install", &json!({ "id": "tiny" }));
assert_eq!((done["id"].clone(), done["source"].clone()), (json!("tiny"), json!("registry")));
assert!(f.config.join("registry/store/grammar/tiny/1.0.0").is_dir());
assert!(!f.config.join("grammars/tiny.sublime-syntax").exists(), "nothing goes to the legacy folder");
assert!(load::is_known("tiny"), "grammar.install reloads the highlighter");
assert_eq!(f.installed(), [("tiny".to_owned(), "registry".to_owned())]);
f.ok("grammar.remove", &json!({ "id": "tiny" }));
assert!(!load::is_known("tiny"));
assert_eq!(f.code("grammar.remove", &json!({ "id": "tiny" })), "not_found");
}
#[test]
fn a_build_with_no_trusted_key_keeps_the_legacy_flow() {
let _turn = turn();
let mut f = fixture("gc-nokey", &[tiny_entry()], &[("grammar", "tiny", vec![("tiny.sublime-syntax", TINY.as_bytes())])], false);
f.session.set_grammar_transport(Box::new(Offline));
f.ok("registry.check", &json!({}));
// The default verifier refuses with `addon`; the command falls through to the old flow, which
// here finds no network (a pasted-link style failure), not the registry's refusal.
assert_eq!(f.code("grammar.install", &json!({ "id": "tiny" })), "offline");
}
#[test]
fn a_pack_grammar_flows_to_the_highlighter() {
let _turn = turn();
let pack = entry("pack", "langs", "1.0.0", &[("grammars/tiny.sublime-syntax", TINY.as_bytes()), ("grammars/other.sublime-syntax", OTHER.as_bytes())], &[]);
let mut f = fixture(
"gc-pack",
&[pack],
&[("pack", "langs", vec![("grammars/tiny.sublime-syntax", TINY.as_bytes()), ("grammars/other.sublime-syntax", OTHER.as_bytes())])],
true,
);
f.ok("registry.check", &json!({}));
f.ok("registry.install", &json!({ "kind": "pack", "id": "langs" }));
f.session.load_grammars();
assert!(load::is_known("tiny") && load::is_known("other"));
}