583 lines
21 KiB
Rust
583 lines
21 KiB
Rust
//! Integration tests of the core commands through [`Registry::run`]: every id is
|
|
//! reachable, the params are validated and the errors are data.
|
|
|
|
mod common;
|
|
|
|
use common::{on_disk, seed, temp_config, temp_vault};
|
|
use notescraft_engine::{CommandError, Error, Registry, Session, adversarial_params, register_core};
|
|
use serde_json::{Value, json};
|
|
|
|
/// Every id the core registers.
|
|
const CORE_IDS: &[&str] = &[
|
|
"base.create",
|
|
"base.list",
|
|
"base.properties",
|
|
"base.query",
|
|
"base.read",
|
|
"base.resolve",
|
|
"base.row.set",
|
|
"base.view.add",
|
|
"base.view.duplicate",
|
|
"base.view.remove",
|
|
"base.view.rename",
|
|
"base.view.reorder",
|
|
"base.write",
|
|
"batch.apply",
|
|
"batch.cancel",
|
|
"batch.decide",
|
|
"batch.proposal",
|
|
"batch.proposals",
|
|
"batch.start",
|
|
"batch.status",
|
|
"batch.step",
|
|
"block.delete",
|
|
"block.duplicate",
|
|
"block.list",
|
|
"block.move",
|
|
"block.turn_into",
|
|
"grammar.info",
|
|
"grammar.install",
|
|
"grammar.install_text",
|
|
"grammar.remove",
|
|
"registry.check",
|
|
"registry.disable",
|
|
"registry.enable",
|
|
"registry.info",
|
|
"registry.install",
|
|
"registry.list",
|
|
"registry.remove",
|
|
"registry.rollback",
|
|
"registry.suggest",
|
|
"registry.update",
|
|
"json.format_block",
|
|
"json.minify_block",
|
|
"vault.info",
|
|
"note.list",
|
|
"note.read",
|
|
"note.write",
|
|
"note.create",
|
|
"note.delete",
|
|
"note.extract",
|
|
"history.list",
|
|
"history.read",
|
|
"trash.empty",
|
|
"workspace.delete",
|
|
"workspace.list",
|
|
"workspace.load",
|
|
"workspace.save",
|
|
"trash.list",
|
|
"trash.restore",
|
|
"note.merge",
|
|
"note.properties",
|
|
"note.property_remove",
|
|
"note.property_rename",
|
|
"note.property_set",
|
|
"properties.keys",
|
|
"properties.rename_all",
|
|
"properties.values",
|
|
// The decorations of the notes: the icon, the tint and the banner (D3).
|
|
"decor.list",
|
|
"decor.set",
|
|
"note.rename",
|
|
"note.duplicate",
|
|
"folder.create",
|
|
"folder.rename",
|
|
"folder.delete",
|
|
"links.outgoing",
|
|
"links.backlinks",
|
|
"links.unresolved",
|
|
"tags.list",
|
|
"tags.rename",
|
|
"tags.merge",
|
|
"buffer.open",
|
|
"buffer.edit",
|
|
"buffer.save",
|
|
"buffer.undo",
|
|
"buffer.redo",
|
|
// Slice 3: search, daily notes and templates, themes.
|
|
"search",
|
|
"switcher.search",
|
|
"note.random",
|
|
"note.unique",
|
|
"daily.open",
|
|
"note.periodic",
|
|
"calendar.month",
|
|
"calendar.agenda",
|
|
"calendar.next",
|
|
"calendar.free",
|
|
"calendar.conflicts",
|
|
"calendar.year",
|
|
"calendar.parse",
|
|
"calendar.review",
|
|
"calendar.filters",
|
|
"calendar.add",
|
|
"calendar.edit",
|
|
"calendar.move",
|
|
"calendar.delete",
|
|
"calendar.note",
|
|
"calendar.bulk",
|
|
"calendar.bulk_delete",
|
|
"calendar.filter_save",
|
|
"calendar.filter_delete",
|
|
"calendar.reminders",
|
|
"calendar.reminders_tick",
|
|
"calendar.reminder_snooze",
|
|
"calendar.reminder_dismiss",
|
|
"calendar.feeds",
|
|
"calendar.feed_add",
|
|
"calendar.feed_remove",
|
|
"calendar.sync_due",
|
|
"calendar.sync_apply",
|
|
"calendar.google_state",
|
|
"knowledge.context",
|
|
"knowledge.rebuild",
|
|
"index.pause",
|
|
"index.resume",
|
|
"knowledge.related",
|
|
"knowledge.search",
|
|
"knowledge.status",
|
|
"lint.check",
|
|
"lint.dismiss",
|
|
"lint.fix",
|
|
"lint.fix_all_safe",
|
|
"lint.rules",
|
|
"lint.state",
|
|
"lint.state_clear",
|
|
"lint.style",
|
|
"calendar.google_calendars",
|
|
"calendar.google_disconnect",
|
|
"template.list",
|
|
"template.create_note",
|
|
"template.insert",
|
|
"theme.list",
|
|
"theme.get",
|
|
"theme.set",
|
|
// The settings document (SPEC 6 item 14).
|
|
"settings.get",
|
|
"settings.set",
|
|
"settings.reset",
|
|
"editor.toggle_vim",
|
|
// The vault catalog behind the launcher (SPEC 19, task 1).
|
|
"vaults.list",
|
|
"note.capture",
|
|
"vaults.pin",
|
|
"vaults.rename",
|
|
"vaults.remove",
|
|
// The WebAssembly plugin host (SPEC 6 item 17).
|
|
"plugins.list",
|
|
"plugins.enable",
|
|
"plugins.disable",
|
|
"git.init",
|
|
"git.status",
|
|
"git.diff",
|
|
"git.stage",
|
|
"git.unstage",
|
|
"git.restore",
|
|
"git.commit",
|
|
"git.log",
|
|
"git.show",
|
|
"git.restore_version",
|
|
"bookmarks.add",
|
|
"bookmarks.remove",
|
|
"bookmarks.list",
|
|
"math.render",
|
|
// PDF export (SPEC 16).
|
|
"export.pdf",
|
|
// The pandoc export: enabled only when pandoc is installed.
|
|
"export.pandoc",
|
|
// Whiteboard SVG export (P12).
|
|
"file.export_whiteboard_svg",
|
|
// Attachments (SPEC 9).
|
|
"attachment.save",
|
|
"attachment.save_path",
|
|
// The slash menu (SPEC 21): the inserts, the 13 callout kinds and plumbing.
|
|
"slash.heading1",
|
|
"slash.heading2",
|
|
"slash.heading3",
|
|
"slash.bullet",
|
|
"slash.numbered",
|
|
"slash.task",
|
|
"slash.quote",
|
|
"slash.code",
|
|
"slash.divider",
|
|
"slash.link_note",
|
|
"slash.embed_note",
|
|
"slash.embed_image",
|
|
"slash.table",
|
|
"slash.callout",
|
|
"slash.callout.note",
|
|
"slash.callout.abstract",
|
|
"slash.callout.info",
|
|
"slash.callout.todo",
|
|
"slash.callout.tip",
|
|
"slash.callout.success",
|
|
"slash.callout.question",
|
|
"slash.callout.warning",
|
|
"slash.callout.failure",
|
|
"slash.callout.danger",
|
|
"slash.callout.bug",
|
|
"slash.callout.example",
|
|
"slash.callout.quote",
|
|
"slash.date",
|
|
"assistant.status",
|
|
"assistant.pending",
|
|
"assistant.accept",
|
|
"assistant.reject",
|
|
"assistant.grant_write",
|
|
"assistant.chat_append",
|
|
"assistant.chat_list",
|
|
"assistant.chat_read",
|
|
"assistant.chat_delete",
|
|
"slash.template",
|
|
"slash.canvas",
|
|
"slash.list",
|
|
"slash.trigger_ok",
|
|
// The sync worker and its conflict surface (SPEC 24).
|
|
"sync.pair",
|
|
"sync.status",
|
|
"sync.pending",
|
|
"sync.now",
|
|
"sync.unlink",
|
|
"sync.conflicts",
|
|
"sync.resolve",
|
|
// The server account, its vaults and the sharing surface (SPEC 19, spec 5-7).
|
|
"server.vaults",
|
|
"server.devices",
|
|
"server.history",
|
|
"server.revision",
|
|
"server.restore",
|
|
"server.create_vault",
|
|
"server.rename_vault",
|
|
"server.delete_vault",
|
|
"server.connect",
|
|
"share.members",
|
|
"share.set_role",
|
|
"share.remove",
|
|
"share.invite",
|
|
"share.invites",
|
|
"share.revoke_invite",
|
|
"share.accept",
|
|
// The published pages and the share links of the bound vault.
|
|
"publish.list",
|
|
"publish.enable",
|
|
"publish.disable",
|
|
"share.link.create",
|
|
"share.link.list",
|
|
"share.link.revoke",
|
|
// The live rooms of the co-editing surface (spec 2).
|
|
"live.join",
|
|
"live.edit",
|
|
"live.poll",
|
|
"live.peers",
|
|
"live.selection",
|
|
"live.info",
|
|
"live.leave",
|
|
"server.file_meta",
|
|
// The file viewers (spec `file-viewers`).
|
|
"file.info",
|
|
"file.hash",
|
|
"file.neighbours",
|
|
"image.annotations.load",
|
|
"image.annotations.save",
|
|
// The review comments of a note and their sidecar (spec comments-and-mentions).
|
|
"comment.list",
|
|
"comment.add",
|
|
"comment.reply",
|
|
"comment.edit",
|
|
"comment.delete",
|
|
"comment.resolve",
|
|
"comment.reanchor",
|
|
// The pending suggestions of a note (spec comments-and-mentions).
|
|
"suggestion.list",
|
|
"suggestion.accept",
|
|
"suggestion.reject",
|
|
// The activity feed and the mention list (spec comments-and-mentions).
|
|
"activity.list",
|
|
"activity.unread",
|
|
"activity.read",
|
|
"mention.candidates",
|
|
// The map tiles of the Maps feature (D13).
|
|
"maps.status",
|
|
"maps.clear_cache",
|
|
"maps.tile",
|
|
// The advanced URI links.
|
|
"uri.resolve",
|
|
];
|
|
|
|
/// A registry with the core commands and a writable session on a small vault.
|
|
fn fixture(name: &str) -> (Registry<Session>, Session, common::TempVault) {
|
|
let vault = temp_vault(name);
|
|
seed(
|
|
vault.path(),
|
|
&[
|
|
("root.md", "# Root\n\n[[notes/Child]] #work\n"),
|
|
("notes/Child.md", "# Child\n\n[back](../root.md#Root) ![[img.png]]\n"),
|
|
("img.png", "not really a png"),
|
|
("free.md", "[[nowhere]]\n"),
|
|
],
|
|
);
|
|
let mut reg = Registry::new();
|
|
register_core(&mut reg).expect("register the core commands");
|
|
let session = Session::open(vault.path()).expect("open the vault");
|
|
(reg, session, vault)
|
|
}
|
|
|
|
/// The `{ code, message }` a command reported.
|
|
fn code(result: &Result<Value, Error>) -> String {
|
|
match result {
|
|
Err(Error::Command { source, .. }) => source.code.clone(),
|
|
other => panic!("expected a command error, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
/// Every core command is registered, described and enabled.
|
|
#[test]
|
|
fn every_core_command_is_registered_and_described() {
|
|
let (reg, session, _vault) = fixture("registered");
|
|
assert_eq!(reg.len(), CORE_IDS.len());
|
|
|
|
let listed = reg.list(&session);
|
|
let ids: Vec<&str> = listed.iter().map(|info| info.id.as_str()).collect();
|
|
let mut sorted: Vec<&str> = CORE_IDS.to_vec();
|
|
sorted.sort_unstable();
|
|
assert_eq!(ids, sorted, "sorted by id");
|
|
for info in &listed {
|
|
assert!(!info.doc.is_empty(), "{} has no doc", info.id);
|
|
if info.id != "export.pandoc" {
|
|
assert!(info.enabled, "{} should be enabled", info.id);
|
|
}
|
|
if info.id.starts_with("buffer.") && !matches!(info.id.as_str(), "buffer.open" | "buffer.undo" | "buffer.redo") {
|
|
assert!(info.params.iter().any(|param| param.required), "{} declares its params", info.id);
|
|
}
|
|
}
|
|
|
|
// Registering the core twice is refused, id by id.
|
|
let mut again = Registry::new();
|
|
register_core(&mut again).expect("register");
|
|
assert!(matches!(register_core(&mut again), Err(Error::Duplicate(id)) if id == "vault.info"));
|
|
}
|
|
|
|
/// The read commands answer with the data the UI and MCP expect.
|
|
#[test]
|
|
fn read_commands_answer_with_json() {
|
|
let (reg, mut session, _vault) = fixture("read");
|
|
|
|
let info = reg.run("vault.info", &mut session, &Value::Null).expect("vault.info");
|
|
assert_eq!(info.get("notes"), Some(&json!(3)));
|
|
assert_eq!(info.get("files"), Some(&json!(1)));
|
|
assert_eq!(info.get("tags"), Some(&json!(1)));
|
|
assert_eq!(info.get("read_only"), Some(&json!(false)));
|
|
assert!(info.get("root").and_then(Value::as_str).is_some_and(|root| !root.is_empty()));
|
|
|
|
let all = reg.run("note.list", &mut session, &Value::Null).expect("note.list");
|
|
assert_eq!(all.get("notes"), Some(&json!(["free.md", "notes/Child.md", "root.md"])));
|
|
let in_notes = reg.run("note.list", &mut session, &json!({ "folder": "notes" })).expect("note.list folder");
|
|
assert_eq!(in_notes.get("notes"), Some(&json!(["notes/Child.md"])));
|
|
let nothing = reg.run("note.list", &mut session, &json!({ "folder": "root" })).expect("a note is not a folder");
|
|
assert_eq!(nothing.get("notes"), Some(&json!([])));
|
|
|
|
let read = reg.run("note.read", &mut session, &json!({ "path": "root.md" })).expect("note.read");
|
|
assert_eq!(read.get("content"), Some(&json!("# Root\n\n[[notes/Child]] #work\n")));
|
|
|
|
let outgoing = reg.run("links.outgoing", &mut session, &json!({ "path": "free.md" })).expect("links.outgoing");
|
|
assert_eq!(outgoing.get("links").and_then(Value::as_array).map(Vec::len), Some(1));
|
|
assert_eq!(outgoing.get("links").and_then(|links| links.get(0)).and_then(|link| link.get("resolved")), Some(&json!(false)));
|
|
|
|
let backlinks = reg.run("links.backlinks", &mut session, &json!({ "path": "notes/Child.md" })).expect("links.backlinks");
|
|
assert_eq!(backlinks.get("count"), Some(&json!(1)));
|
|
|
|
let unresolved = reg.run("links.unresolved", &mut session, &Value::Null).expect("links.unresolved");
|
|
assert_eq!(unresolved.get("count"), Some(&json!(1)));
|
|
assert_eq!(unresolved.get("links").and_then(|links| links.get(0)).and_then(|link| link.get("source")), Some(&json!("free.md")));
|
|
|
|
let tags = reg.run("tags.list", &mut session, &Value::Null).expect("tags.list");
|
|
let root_tag = tags.get("tags").and_then(|tags| tags.get(0));
|
|
assert_eq!(root_tag.and_then(|tag| tag.get("tag")), Some(&json!("work")));
|
|
assert_eq!(root_tag.and_then(|tag| tag.get("count")), Some(&json!(1)));
|
|
let prefixed = reg.run("tags.list", &mut session, &json!({ "prefix": "nope" })).expect("tags.list prefix");
|
|
assert_eq!(prefixed.get("tags"), Some(&json!([])));
|
|
}
|
|
|
|
/// The write commands go through the vault and the index.
|
|
#[test]
|
|
fn write_commands_change_the_vault() {
|
|
let (reg, mut session, vault) = fixture("write");
|
|
|
|
reg.run("note.create", &mut session, &json!({ "path": "sub/new.md", "content": "# New\n" })).expect("note.create");
|
|
assert_eq!(on_disk(vault.path(), "sub/new.md"), "# New\n");
|
|
assert_eq!(code(®.run("note.create", &mut session, &json!({ "path": "sub/new.md" }))), "exists");
|
|
assert_eq!(code(®.run("note.create", &mut session, &json!({ "path": "sub/new.txt" }))), "invalid_path");
|
|
|
|
let written = reg.run("note.write", &mut session, &json!({ "path": "sub/new.md", "content": "# New\n\n[[sub/new]]\n" })).expect("note.write");
|
|
assert_eq!(written.get("changed"), Some(&json!(true)));
|
|
assert_eq!(session.index().note("sub/new.md").map(|note| note.links.len()), Some(1));
|
|
|
|
let renamed = reg.run("note.rename", &mut session, &json!({ "from": "sub/new.md", "to": "sub/renamed.md" })).expect("note.rename");
|
|
assert_eq!(renamed.get("rewritten"), Some(&json!(["sub/renamed.md"])), "its own self-link moved with it");
|
|
assert_eq!(on_disk(vault.path(), "sub/renamed.md"), "# New\n\n[[sub/renamed]]\n");
|
|
let no_links = reg.run("note.rename", &mut session, &json!({ "from": "free.md", "to": "free2.md", "update_links": false })).expect("note.rename");
|
|
assert_eq!(no_links.get("rewritten"), Some(&json!([])));
|
|
|
|
let deleted = reg.run("note.delete", &mut session, &json!({ "path": "free2.md" })).expect("note.delete");
|
|
assert_eq!(deleted.get("deleted"), Some(&json!(true)));
|
|
assert!(session.index().note("free2.md").is_none());
|
|
assert_eq!(code(®.run("note.delete", &mut session, &json!({ "path": "free2.md" }))), "not_found");
|
|
}
|
|
|
|
/// The buffer commands edit, save and undo through the vault.
|
|
#[test]
|
|
fn buffer_commands_edit_the_vault() {
|
|
let (reg, mut session, vault) = fixture("buffers");
|
|
|
|
let opened = reg.run("buffer.open", &mut session, &json!({ "path": "root.md" })).expect("buffer.open");
|
|
assert_eq!(opened.get("dirty"), Some(&json!(false)));
|
|
|
|
let edited = reg.run("buffer.edit", &mut session, &json!({ "path": "root.md", "start": 0, "end": 6, "text": "Start" })).expect("buffer.edit");
|
|
assert_eq!(edited.get("dirty"), Some(&json!(true)));
|
|
assert_eq!(on_disk(vault.path(), "root.md"), "# Root\n\n[[notes/Child]] #work\n", "not saved yet");
|
|
|
|
let saved = reg.run("buffer.save", &mut session, &json!({ "path": "root.md" })).expect("buffer.save");
|
|
assert_eq!(saved.get("changed"), Some(&json!(true)));
|
|
assert_eq!(on_disk(vault.path(), "root.md"), "Start\n\n[[notes/Child]] #work\n");
|
|
|
|
let undone = reg.run("buffer.undo", &mut session, &json!({ "path": "root.md" })).expect("buffer.undo");
|
|
assert_eq!(undone.get("undone"), Some(&json!(true)));
|
|
assert_eq!(undone.get("dirty"), Some(&json!(true)));
|
|
let redone = reg.run("buffer.redo", &mut session, &json!({ "path": "root.md" })).expect("buffer.redo");
|
|
assert_eq!(redone.get("redone"), Some(&json!(true)));
|
|
assert_eq!(reg.run("buffer.save", &mut session, &json!({ "path": "root.md" })).expect("save").get("changed"), Some(&json!(false)));
|
|
|
|
assert_eq!(code(®.run("buffer.edit", &mut session, &json!({ "path": "root.md", "start": 0, "end": 9999, "text": "x" }))), "invalid_path");
|
|
assert_eq!(on_disk(vault.path(), "root.md"), "Start\n\n[[notes/Child]] #work\n", "the refused edit changed nothing");
|
|
}
|
|
|
|
/// A read-only session disables the write commands and refuses their bodies.
|
|
#[test]
|
|
fn a_read_only_session_disables_every_write_command() {
|
|
let vault = temp_vault("read-only-commands");
|
|
seed(vault.path(), &[("note.md", "body\n")]);
|
|
let mut reg = Registry::new();
|
|
register_core(&mut reg).expect("register");
|
|
let mut session = Session::open_read_only(vault.path()).expect("open read-only");
|
|
|
|
for id in ["note.write", "note.create", "note.delete", "note.rename", "buffer.edit", "buffer.save"] {
|
|
assert!(!reg.is_enabled(id, &session), "{id} must be disabled");
|
|
assert!(matches!(reg.run(id, &mut session, &json!({ "path": "note.md" })), Err(Error::Disabled(_))), "{id} must not run");
|
|
}
|
|
for id in [
|
|
"vault.info",
|
|
"note.list",
|
|
"note.read",
|
|
"links.outgoing",
|
|
"links.backlinks",
|
|
"links.unresolved",
|
|
"tags.list",
|
|
"buffer.open",
|
|
"settings.get",
|
|
"settings.set",
|
|
"settings.reset",
|
|
] {
|
|
assert!(reg.is_enabled(id, &session), "{id} must stay enabled");
|
|
}
|
|
assert!(reg.run("note.read", &mut session, &json!({ "path": "note.md" })).is_ok());
|
|
assert!(reg.run("buffer.open", &mut session, &json!({ "path": "note.md" })).is_ok());
|
|
}
|
|
|
|
/// The vault refuses the reserved folders and the command surfaces that error.
|
|
#[test]
|
|
fn reserved_paths_are_refused_by_the_command() {
|
|
let (reg, mut session, vault) = fixture("reserved");
|
|
seed(vault.path(), &[(".obsidian/app.json", "{}")]);
|
|
assert!(vault.path().join(".obsidian/app.json").exists());
|
|
|
|
for id in [".obsidian/x.md", ".notescraft/y.md"] {
|
|
assert_eq!(code(®.run("note.write", &mut session, &json!({ "path": id, "content": "x" }))), "reserved", "note.write {id}");
|
|
assert_eq!(code(®.run("note.create", &mut session, &json!({ "path": id }))), "reserved", "note.create {id}");
|
|
assert_eq!(code(®.run("note.delete", &mut session, &json!({ "path": id }))), "reserved", "note.delete {id}");
|
|
assert_eq!(code(®.run("note.rename", &mut session, &json!({ "from": "root.md", "to": id }))), "reserved", "note.rename into {id}");
|
|
assert_eq!(code(®.run("note.rename", &mut session, &json!({ "from": id, "to": "b.md" }))), "not_found", "a reserved path is never a note");
|
|
assert!(!vault.path().join(id).exists(), "{id} must not exist");
|
|
}
|
|
// A reserved path that is not a note is refused on the extension first.
|
|
assert_eq!(code(®.run("note.write", &mut session, &json!({ "path": ".obsidian/app.json", "content": "x" }))), "invalid_path");
|
|
assert_eq!(on_disk(vault.path(), ".obsidian/app.json"), "{}", "Obsidian's folder is untouched");
|
|
}
|
|
|
|
/// Values the registry cannot type-check are refused before the body runs.
|
|
#[test]
|
|
fn bad_params_are_refused() {
|
|
let (reg, mut session, _vault) = fixture("bad-params");
|
|
|
|
for params in [
|
|
json!(null),
|
|
json!({}),
|
|
json!({ "path": 3 }),
|
|
json!({ "path": "note.md", "typo": 1 }),
|
|
json!({ "path": "../escape.md" }),
|
|
json!({ "path": "" }),
|
|
json!({ "path": "/abs/note.md" }),
|
|
] {
|
|
assert!(reg.run("note.write", &mut session, ¶ms).is_err(), "note.write must refuse {params}");
|
|
}
|
|
assert!(matches!(reg.run("note.read", &mut session, &json!({ "path": "nope.md" })), Err(Error::Command { .. })));
|
|
assert!(matches!(
|
|
reg.run("note.rename", &mut session, &json!({ "from": "root.md", "to": "root.md", "update_links": 3 })),
|
|
Err(Error::InvalidParams { .. })
|
|
));
|
|
assert_eq!(code(®.run("buffer.edit", &mut session, &json!({ "path": "root.md", "start": -1, "end": 2, "text": "x" }))), "invalid_params");
|
|
assert!(matches!(reg.run("buffer.edit", &mut session, &json!({ "path": "root.md", "start": 0, "end": 4, "text": 7 })), Err(Error::InvalidParams { .. })));
|
|
assert_eq!(code(®.run("buffer.undo", &mut session, &json!({ "path": "no-buffer.md" }))), "not_found");
|
|
assert!(matches!(reg.run("no.such.command", &mut session, &Value::Null), Err(Error::UnknownCommand(_))));
|
|
}
|
|
|
|
/// No adversarial params object can panic a core command or leave the vault broken.
|
|
#[test]
|
|
fn adversarial_params_never_panic_a_core_command() {
|
|
let vault = temp_vault("fuzz");
|
|
seed(
|
|
vault.path(),
|
|
&[
|
|
("root.md", "# Root\n\n[[notes/Child]] #work\n"),
|
|
("notes/Child.md", "# Child\n\n[back](../root.md#Root) ![[img.png]]\n"),
|
|
("img.png", "not really a png"),
|
|
("free.md", "[[nowhere]]\n"),
|
|
],
|
|
);
|
|
let config = temp_config("fuzz");
|
|
let mut reg = Registry::new();
|
|
register_core(&mut reg).expect("register the core commands");
|
|
let mut session = Session::open(vault.path()).expect("open the vault");
|
|
session.use_settings_dir(config.path());
|
|
let previous = std::panic::take_hook();
|
|
std::panic::set_hook(Box::new(|_| {}));
|
|
let mut accepted = 0_usize;
|
|
let mut panicked = Vec::new();
|
|
for id in CORE_IDS {
|
|
for params in adversarial_params() {
|
|
match reg.run(id, &mut session, ¶ms) {
|
|
Ok(_) => accepted += 1,
|
|
Err(Error::Panicked(other)) => panicked.push(other),
|
|
Err(_) => {}
|
|
}
|
|
}
|
|
}
|
|
std::panic::set_hook(previous);
|
|
|
|
assert!(panicked.is_empty(), "commands panicked: {panicked:?}");
|
|
assert!(accepted > 0, "at least the param-less commands accept some fixtures");
|
|
assert!(reg.run("vault.info", &mut session, &Value::Null).is_ok(), "the registry is still usable");
|
|
assert_eq!(reg.len(), CORE_IDS.len());
|
|
}
|
|
|
|
/// The command error payload is the documented `{ code, message }` shape.
|
|
#[test]
|
|
fn command_errors_carry_a_code_and_a_message() {
|
|
let (reg, mut session, _vault) = fixture("errors");
|
|
let Err(Error::Command { source, .. }) = reg.run("note.read", &mut session, &json!({ "path": "nope.md" })) else {
|
|
panic!("expected a command error");
|
|
};
|
|
assert_eq!(source.code, "not_found");
|
|
assert!(source.message.contains("nope.md"), "{}", source.message);
|
|
assert_eq!(source, CommandError::new(source.code.clone(), source.message.clone()));
|
|
}
|