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

146 lines
6.9 KiB
Rust

//! The vault `wanted` scan: fence labels and math in the notes become `wanted` ids in
//! `.notescraft/registry.json`; installed items are not listed; the budget ends a scan early;
//! nothing touches the network.
mod addons_support;
mod common;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use addons_support::{Shared, TINY, entry, index_json, serve};
use common::{TempVault, seed, temp_config, temp_vault};
use notescraft_engine::addons::{FakeTransport, FakeVerifier, INDEX_URL, Response, VaultFile, record_wanted, record_wanted_once, scan_usage, scan_wanted};
use notescraft_engine::{Registry, Session, register_core};
use serde_json::{Value, json};
/// The highlighter's grammar list is process-wide, so these tests take turns.
static TURN: Mutex<()> = Mutex::new(());
/// A grammar no build bundles, so the scan treats its fence label as missing.
const ID: &str = "zorkscript";
const FONT: &[u8] = &[0, 1, 0, 0, 9, 9];
const REGISTRY_JSON: &str = ".notescraft/registry.json";
struct Fixture {
reg: Registry<Session>,
session: Session,
net: Arc<FakeTransport>,
vault: std::path::PathBuf,
_guards: (TempVault, TempVault),
}
fn fixture(name: &str, notes: &[(&str, &str)]) -> Fixture {
let (vault, config) = (temp_vault(name), temp_config(name));
seed(vault.path(), notes);
let file = format!("{ID}.sublime-syntax");
let entries = [entry("grammar", ID, "1.0.0", &[(&file, TINY.as_bytes())], &[]), entry("font", "stix-math", "1.0.0", &[("stix.ttf", FONT)], &[])];
let net = FakeTransport::new().with_response(INDEX_URL, Ok(Response { status: 200, etag: None, bytes: index_json(&entries, &[]) }));
let net = Arc::new(serve(serve(net, "grammar", ID, "1.0.0", &[(&file, TINY.as_bytes())]), "font", "stix-math", "1.0.0", &[("stix.ttf", FONT)]));
let mut session = Session::open(vault.path()).unwrap();
session.use_settings_dir(config.path());
session.set_addons_transport(Box::new(Shared(net.clone())));
session.set_addons_verifier(Box::new(FakeVerifier { accept: true }));
let mut reg = Registry::new();
register_core(&mut reg).unwrap();
let path = vault.path().to_path_buf();
Fixture { reg, session, net, vault: path, _guards: (vault, config) }
}
impl Fixture {
fn ok(&mut self, id: &str, params: &Value) -> Value {
self.reg.run(id, &mut self.session, params).unwrap_or_else(|e| panic!("{id}: {e}"))
}
}
fn fenced(label: &str) -> String {
format!("# Note\n\n```{label}\nfoo\n```\n")
}
#[test]
fn a_fence_label_of_an_indexed_grammar_is_wanted() {
let _turn = TURN.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let mut f = fixture("wn-fence", &[("a.md", &fenced(ID)), ("b.md", &fenced("rust")), ("c.md", &fenced("nothing-indexed"))]);
assert_eq!(scan_wanted(&f.session, 5000).unwrap(), Vec::<String>::new(), "no cached index, no answer");
f.ok("registry.check", &json!({}));
assert_eq!(scan_wanted(&f.session, 5000).unwrap(), [format!("grammar:{ID}")]);
}
#[test]
fn a_label_only_counts_where_a_fence_opens() {
let _turn = TURN.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let text = format!("```text\n```{ID}\n~~~{ID}\n```\n\nplain {ID}\n");
let mut f = fixture("wn-nest", &[("a.md", &text)]);
f.ok("registry.check", &json!({}));
assert!(scan_wanted(&f.session, 5000).unwrap().is_empty(), "the lookalike lines are inside a `text` fence");
}
#[test]
fn installed_items_are_not_listed() {
let _turn = TURN.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let mut f = fixture("wn-installed", &[("a.md", &fenced(ID))]);
f.ok("registry.check", &json!({}));
f.ok("registry.install", &json!({ "kind": "grammar", "id": ID }));
assert_eq!(scan_wanted(&f.session, 5000).unwrap(), Vec::<String>::new());
f.ok("registry.remove", &json!({ "kind": "grammar", "id": ID }));
f.session.load_grammars();
assert_eq!(scan_wanted(&f.session, 5000).unwrap(), [format!("grammar:{ID}")]);
}
#[test]
fn math_wants_the_math_font_but_prices_do_not() {
let _turn = TURN.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let mut f = fixture("wn-math", &[("price.md", "It costs $5 and then $6 more.\n")]);
f.ok("registry.check", &json!({}));
assert_eq!(scan_wanted(&f.session, 5000).unwrap(), Vec::<String>::new());
seed(&f.vault, &[("math.md", "Euler: $e^{i\\pi}+1=0$\n")]);
assert_eq!(scan_wanted(&f.session, 5000).unwrap(), ["font:stix-math"]);
seed(&f.vault, &[("math.md", "$$\nx\n$$\n")]);
assert_eq!(scan_wanted(&f.session, 5000).unwrap(), ["font:stix-math"]);
}
#[test]
fn the_budget_ends_a_big_scan_early_without_error() {
let _turn = TURN.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let notes: Vec<(String, String)> = (0..400).map(|n| (format!("n{n}.md"), fenced(ID))).collect();
let refs: Vec<(&str, &str)> = notes.iter().map(|(a, b)| (a.as_str(), b.as_str())).collect();
let mut f = fixture("wn-budget", &refs);
f.ok("registry.check", &json!({}));
let cut = scan_usage(&f.vault, Duration::ZERO);
assert!(cut.cut_short && cut.labels.is_empty());
assert!(scan_wanted(&f.session, 0).unwrap().is_empty(), "a spent budget is an empty answer, not an error");
let full = scan_usage(&f.vault, Duration::from_secs(30));
assert!(!full.cut_short && full.labels.contains(ID));
}
#[test]
fn notes_over_two_mib_are_not_read() {
let _turn = TURN.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let big = format!("{}{}", fenced(ID), "x".repeat(2 * 1024 * 1024));
let f = fixture("wn-big", &[("big.md", &big)]);
assert!(scan_usage(&f.vault, Duration::from_secs(30)).labels.is_empty());
}
#[test]
fn record_writes_wanted_keeps_enabled_never_uses_the_network_and_runs_once_per_generation() {
let _turn = TURN.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let mut f = fixture("wn-record", &[("a.md", &fenced(ID))]);
f.ok("registry.check", &json!({}));
let asked = f.net.asked().len();
seed(&f.vault, &[(REGISTRY_JSON, r#"{"version":1,"enabled":["plugin:dice"],"wanted":[]}"#)]);
assert_eq!(record_wanted_once(&f.session, 5000).unwrap(), Some(vec![format!("grammar:{ID}")]));
let file = VaultFile::parse(&std::fs::read(f.vault.join(REGISTRY_JSON)).unwrap()).unwrap();
assert_eq!((file.enabled, file.wanted), (vec!["plugin:dice".to_owned()], vec![format!("grammar:{ID}")]));
assert_eq!(record_wanted_once(&f.session, 5000).unwrap(), None, "same vault, same index generation");
assert_eq!(f.net.asked().len(), asked, "the scan never touches the network");
}
#[test]
fn a_corrupt_registry_json_is_left_untouched() {
let _turn = TURN.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let mut f = fixture("wn-corrupt", &[("a.md", &fenced(ID)), (REGISTRY_JSON, "{ nope")]);
f.ok("registry.check", &json!({}));
assert!(record_wanted(&f.session, 5000).is_err());
assert_eq!(std::fs::read_to_string(f.vault.join(REGISTRY_JSON)).unwrap(), "{ nope");
}