Files
Marcos Paulo 4ea2e9fd5a
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: outliner list moves, notescraft:// URI resolve, periodic notes (MiniMax M3.1 Flash)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 05:54:18 -03:00

173 lines
7.3 KiB
Rust

//! The advanced URI surface: the `notescraft://` links and the `uri.resolve`
//! command.
mod common;
use common::{seed, temp_vault};
use notescraft_engine::uri::{SCHEME, UriAction, format_open_uri, parse};
use notescraft_engine::{Error, Registry, Session, register_core};
use serde_json::{Value, json};
/// The vault name of a temp vault: the folder name, as the UI shows it.
fn vault_name(vault: &common::TempVault) -> String {
vault.path().file_name().map(|name| name.to_string_lossy().into_owned()).unwrap_or_default()
}
/// A registry with the core commands and a 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"), ("notes/Child.md", "# Child\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 error code 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 action of the grammar is parsed.
#[test]
fn every_action_is_parsed() {
let open = parse("notescraft://open?vault=My%20Vault&file=notes/Child.md&heading=Child&block=%5Eabc&line=12").expect("open");
assert_eq!(
open,
UriAction::Open {
vault: "My Vault".to_owned(),
file: "notes/Child.md".to_owned(),
heading: Some("Child".to_owned()),
block: Some("^abc".to_owned()),
line: Some(12),
}
);
let new = parse("notescraft://new?vault=V&name=Inbox/New.md&content=Hello%20world").expect("new");
assert_eq!(new, UriAction::New { vault: "V".to_owned(), name: "Inbox/New.md".to_owned(), content: "Hello world".to_owned() });
let daily = parse("notescraft://daily?vault=V").expect("daily");
assert_eq!(daily, UriAction::Daily { vault: "V".to_owned() });
let search = parse("notescraft://search?vault=V&query=rust").expect("search");
assert_eq!(search, UriAction::Search { vault: "V".to_owned(), query: "rust".to_owned() });
}
/// Percent-encoded UTF-8 comes back as it was written.
#[test]
fn percent_encoded_utf8_is_decoded() {
let action = parse("notescraft://open?vault=Notes&file=Di%C3%A1rios%2FA%C3%A7%C3%A3o.md").expect("open");
assert_eq!(
action,
UriAction::Open { vault: "Notes".to_owned(), file: "Di\u{e1}rios/A\u{e7}\u{e3}o.md".to_owned(), heading: None, block: None, line: None }
);
let search = parse("notescraft://search?vault=Notes&query=a+b").expect("search");
assert_eq!(search, UriAction::Search { vault: "Notes".to_owned(), query: "a+b".to_owned() }, "a plus is not a space");
}
/// The paths a link may not carry are refused.
#[test]
fn hostile_paths_are_refused() {
for url in [
"notescraft://open?vault=V&file=../escape.md",
"notescraft://open?vault=V&file=a/../../escape.md",
"notescraft://open?vault=V&file=/etc/passwd.md",
"notescraft://open?vault=V&file=.obsidian/app.md",
"notescraft://open?vault=V&file=.notescraft/x.md",
"notescraft://new?vault=V&name=.notescraft/x.md",
"notescraft://open?vault=V&file=root%00.md",
"notescraft://open?vault=V&file=root.txt",
] {
let error = parse(url).expect_err("the link must be refused");
assert_eq!(error.code, "invalid_params", "{url}");
}
}
/// A link over the cap is refused before it is looked at.
#[test]
fn a_long_link_is_refused() {
let url = format!("notescraft://open?vault=V&file={}.md", "a".repeat(5000));
let error = parse(&url).expect_err("the link must be refused");
assert_eq!(error.code, "invalid_params");
}
/// Only the four actions are supported; a link never runs a command.
#[test]
fn an_unknown_action_is_refused() {
for url in ["notescraft://delete?vault=V", "notescraft://", "notescraft://open?vault=V&file=a.md&typo=1", "https://example.com"] {
assert!(parse(url).is_err(), "{url}");
}
let error = parse("notescraft://command?id=note.write").expect_err("a link never runs a command");
assert_eq!(error.code, "invalid_params");
assert!(error.message.contains("command"), "{}", error.message);
}
/// The command reports the note the link points at.
#[test]
fn the_resolve_command_reports_the_note() {
let (reg, mut session, vault) = fixture("resolve");
let name = vault_name(&vault);
let resolved = reg.run("uri.resolve", &mut session, &json!({ "url": format_open_uri(&name, "root.md", None) })).expect("uri.resolve");
assert_eq!(resolved.get("action"), Some(&json!("open")));
assert_eq!(resolved.get("vault"), Some(&json!(name)));
assert_eq!(resolved.get("path"), Some(&json!("root.md")));
assert_eq!(resolved.get("exists"), Some(&json!(true)));
assert_eq!(resolved.get("needs_confirmation"), Some(&json!(false)));
let missing = reg.run("uri.resolve", &mut session, &json!({ "url": format_open_uri(&name, "nope.md", None) })).expect("uri.resolve");
assert_eq!(missing.get("exists"), Some(&json!(false)));
}
/// A vault that is not open is not found.
#[test]
fn an_unknown_vault_is_not_found() {
let (reg, mut session, _vault) = fixture("unknown-vault");
let result = reg.run("uri.resolve", &mut session, &json!({ "url": "notescraft://open?vault=other&file=root.md" }));
assert_eq!(code(&result), "not_found");
}
/// A link that would create a note asks first, and writes nothing.
#[test]
fn a_new_link_asks_for_confirmation() {
let (reg, mut session, vault) = fixture("new-link");
let name = vault_name(&vault);
let url = format!("{SCHEME}new?vault={name}&name=Inbox/New.md&content=Hello");
let resolved = reg.run("uri.resolve", &mut session, &json!({ "url": url })).expect("uri.resolve");
assert_eq!(resolved.get("action"), Some(&json!("new")));
assert_eq!(resolved.get("path"), Some(&json!("Inbox/New.md")));
assert_eq!(resolved.get("needs_confirmation"), Some(&json!(true)));
assert_eq!(resolved.get("content"), Some(&json!("Hello")));
assert!(!vault.path().join("Inbox/New.md").exists(), "the link wrote nothing");
}
/// A daily link reports the note of today.
#[test]
fn a_daily_link_reports_todays_note() {
let (reg, mut session, vault) = fixture("daily");
let name = vault_name(&vault);
let resolved = reg.run("uri.resolve", &mut session, &json!({ "url": format!("{SCHEME}daily?vault={name}") })).expect("uri.resolve");
assert_eq!(resolved.get("action"), Some(&json!("daily")));
let path = resolved.get("path").and_then(Value::as_str).unwrap_or_default();
assert!(path.starts_with("Daily/"), "{path}");
assert!(path.to_lowercase().ends_with(".md"), "{path}");
}
/// The helper the UI copies round trips through the parser.
#[test]
fn the_format_helper_round_trips() {
let url = format_open_uri("My Vault", "a/b c.md", Some("Head ing"));
assert_eq!(url, "notescraft://open?vault=My%20Vault&file=a%2Fb%20c.md&heading=Head%20ing");
assert_eq!(
parse(&url).expect("the link"),
UriAction::Open { vault: "My Vault".to_owned(), file: "a/b c.md".to_owned(), heading: Some("Head ing".to_owned()), block: None, line: None }
);
assert_eq!(format_open_uri("V", "a.md", None), "notescraft://open?vault=V&file=a.md");
}