5e8020aa38
Editor previews (colour swatches, arrows, embeds), the 18 obsidian-basics tasks (find and replace with regex, back/forward, auto-pair, footnotes, folding, search operators, split view, PDF/audio/ video embeds, spelling, drag and drop, pinned tabs, settings) and round 2: ```query blocks, stacked tabs, pop-out windows, tasks ticked inside embeds, text around an embed, embeds that follow edits, block menu shortcut. New crates: notescraft-regex (linear-time engine) and notescraft-syntax. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
381 lines
20 KiB
Rust
381 lines
20 KiB
Rust
//! The `batch.*` commands: queue a scope, step through it, decide and apply.
|
|
|
|
mod common;
|
|
|
|
use std::path::Path;
|
|
use std::time::Duration;
|
|
|
|
use common::{on_disk, seed, temp_vault};
|
|
use notescraft_engine::{Error, Registry, Session, SessionConfig, register_core};
|
|
use serde_json::{Value, json};
|
|
|
|
/// A note whose `json` block is not formatted yet.
|
|
const JSON_NOTE: &str = "# J\n\n```json\n{\"b\":1,\"a\":2}\n```\n";
|
|
/// The same note after the block is formatted.
|
|
const JSON_FORMATTED: &str = "# J\n\n```json\n{\n \"b\": 1,\n \"a\": 2\n}\n```\n";
|
|
/// An unlabeled fence holding JSON: the change needs review.
|
|
const UNLABELED_NOTE: &str = "# U\n\n```\n{\"b\":1}\n```\n";
|
|
/// A single trailing space: `trailing-space` fires with one safe edit.
|
|
const LINT_NOTE: &str = "# L\n\ntrailing space \n";
|
|
const LINT_FIXED: &str = "# L\n\ntrailing space\n";
|
|
const CLEAN_NOTE: &str = "# C\n\na clean note\n";
|
|
|
|
const MAIN_NOTES: &[(&str, &str)] = &[("json.md", JSON_NOTE), ("unlabeled.md", UNLABELED_NOTE), ("lint.md", LINT_NOTE), ("clean.md", CLEAN_NOTE)];
|
|
|
|
/// A registry and a session on a seeded vault.
|
|
fn setup(name: &str, files: &[(&str, &str)]) -> (Registry<Session>, Session, common::TempVault) {
|
|
let vault = temp_vault(name);
|
|
seed(vault.path(), files);
|
|
let mut registry = Registry::new();
|
|
register_core(&mut registry).expect("register");
|
|
let session = Session::open(vault.path()).expect("open");
|
|
(registry, session, vault)
|
|
}
|
|
|
|
fn run(registry: &Registry<Session>, session: &mut Session, id: &str, params: &Value) -> Value {
|
|
registry.run(id, session, params).unwrap_or_else(|error| panic!("{id}: {error:?}"))
|
|
}
|
|
|
|
/// Steps until the batch finishes and returns the last progress.
|
|
fn drain(registry: &Registry<Session>, session: &mut Session) -> Value {
|
|
loop {
|
|
let progress = run(registry, session, "batch.step", &json!({ "budget_ms": 1 }));
|
|
if progress["finished"] == json!(true) {
|
|
return progress;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The bytes of every note, to prove the batch wrote nothing.
|
|
fn snapshot(vault: &Path, files: &[(&str, &str)]) -> Vec<(String, Vec<u8>)> {
|
|
files.iter().map(|(path, _)| ((*path).to_owned(), std::fs::read(vault.join(path)).unwrap_or_default())).collect()
|
|
}
|
|
|
|
/// The error code of a refused command call.
|
|
fn code(result: Result<Value, Error>) -> String {
|
|
match result {
|
|
Err(Error::Command { source, .. }) => source.code.clone(),
|
|
Err(other) => panic!("expected a command error, got {other:?}"),
|
|
Ok(value) => panic!("expected a refusal, got {value}"),
|
|
}
|
|
}
|
|
|
|
/// The row of `path` in a `batch.proposals` answer.
|
|
fn row(rows: &[Value], path: &str) -> Value {
|
|
rows.iter().find(|row| row["path"] == json!(path)).cloned().unwrap_or_else(|| panic!("no row for {path}"))
|
|
}
|
|
|
|
#[test]
|
|
fn start_and_step_review_without_writing() {
|
|
let (registry, mut session, vault) = setup("batch-review", MAIN_NOTES);
|
|
let before = snapshot(vault.path(), MAIN_NOTES);
|
|
|
|
let started = run(®istry, &mut session, "batch.start", &json!({ "scope": "vault" }));
|
|
assert_eq!(started, json!({ "total": 4 }));
|
|
let progress = drain(®istry, &mut session);
|
|
assert_eq!(progress["total"], json!(4));
|
|
assert_eq!(progress["done"], json!(4));
|
|
assert_eq!(progress["finished"], json!(true));
|
|
assert_eq!(progress["proposed"], json!(2), "json.md and lint.md");
|
|
assert_eq!(progress["needs_review"], json!(1), "unlabeled.md");
|
|
assert_eq!(progress["clean"], json!(1), "clean.md");
|
|
assert_eq!(progress["not_edited"], json!(0));
|
|
assert_eq!(snapshot(vault.path(), MAIN_NOTES), before, "reading the batch writes nothing");
|
|
assert_eq!(run(®istry, &mut session, "batch.status", &Value::Null), progress, "status changes nothing");
|
|
|
|
let list = run(®istry, &mut session, "batch.proposals", &Value::Null);
|
|
let rows = list["proposals"].as_array().expect("rows").clone();
|
|
assert_eq!(rows.len(), 4);
|
|
assert_eq!(row(&rows, "json.md")["status"], json!("proposed"));
|
|
assert_eq!(row(&rows, "json.md")["changes"], json!(1));
|
|
assert_eq!(row(&rows, "unlabeled.md")["status"], json!("needs_review"));
|
|
assert_eq!(row(&rows, "clean.md")["status"], json!("clean"));
|
|
assert_eq!(row(&rows, "clean.md")["changes"], json!(0));
|
|
assert_eq!(list["truncated"], json!(false));
|
|
let only_clean = run(®istry, &mut session, "batch.proposals", &json!({ "status": "clean" }));
|
|
assert_eq!(only_clean["proposals"].as_array().expect("rows").len(), 1);
|
|
|
|
let full = run(®istry, &mut session, "batch.proposal", &json!({ "path": "json.md" }));
|
|
assert_eq!(full["path"], json!("json.md"));
|
|
assert_eq!(full["status"], json!("Proposed"));
|
|
let change = &full["changes"][0];
|
|
let at = JSON_NOTE.find("{\"b\":1,\"a\":2}").expect("the JSON core");
|
|
assert_eq!(change["id"], json!(0));
|
|
assert_eq!(change["kind"], json!("Json"));
|
|
assert_eq!(change["range"], json!({ "start": at, "end": at + "{\"b\":1,\"a\":2}".len() }));
|
|
assert_eq!(change["before"], json!("{\"b\":1,\"a\":2}"));
|
|
assert_eq!(change["after"], json!("{\n \"b\": 1,\n \"a\": 2\n}"));
|
|
assert_eq!(change["safe"], json!(true));
|
|
assert_eq!(change["decision"], json!("Pending"));
|
|
assert_eq!(change["rule"], Value::Null);
|
|
|
|
let lint_full = run(®istry, &mut session, "batch.proposal", &json!({ "path": "lint.md" }));
|
|
let lint_change = &lint_full["changes"][0];
|
|
assert_eq!(lint_change["kind"], json!("Lint"));
|
|
assert_eq!(lint_change["rule"], json!("trailing-space"));
|
|
assert_eq!(lint_change["safe"], json!(true));
|
|
}
|
|
|
|
#[test]
|
|
fn decide_then_apply_writes_only_the_accepted_changes() {
|
|
let (registry, mut session, vault) = setup("batch-apply", MAIN_NOTES);
|
|
run(®istry, &mut session, "batch.start", &json!({ "scope": "vault" }));
|
|
drain(®istry, &mut session);
|
|
|
|
let accepted = run(®istry, &mut session, "batch.decide", &json!({ "path": "json.md", "change_id": 0, "decision": "accept" }));
|
|
assert_eq!(accepted["decided"], json!(1));
|
|
let skipped = run(®istry, &mut session, "batch.decide", &json!({ "path": "lint.md", "decision": "skip" }));
|
|
assert_eq!(skipped["decided"], json!(1));
|
|
|
|
let applied = run(®istry, &mut session, "batch.apply", &Value::Null);
|
|
assert_eq!(applied["written"], json!(1));
|
|
assert_eq!(applied["skipped"], json!([]));
|
|
assert_eq!(applied["failed"], json!([]));
|
|
assert_eq!(on_disk(vault.path(), "json.md"), JSON_FORMATTED);
|
|
assert_eq!(on_disk(vault.path(), "lint.md"), LINT_NOTE, "the skipped fix is not applied");
|
|
assert_eq!(on_disk(vault.path(), "unlabeled.md"), UNLABELED_NOTE, "pending is not applied");
|
|
assert_eq!(on_disk(vault.path(), "clean.md"), CLEAN_NOTE);
|
|
|
|
let clean = run(®istry, &mut session, "batch.proposals", &json!({ "status": "clean" }));
|
|
let rows = clean["proposals"].as_array().expect("rows").clone();
|
|
assert_eq!(rows.len(), 2, "json.md became clean; clean.md already was");
|
|
assert_eq!(row(&rows, "json.md")["changes"], json!(0), "the written note has no changes left");
|
|
|
|
let again = run(®istry, &mut session, "batch.apply", &Value::Null);
|
|
assert_eq!(again["written"], json!(0));
|
|
}
|
|
|
|
#[test]
|
|
fn all_true_accepts_every_change_of_the_proposed_notes() {
|
|
let (registry, mut session, vault) = setup("batch-all", MAIN_NOTES);
|
|
run(®istry, &mut session, "batch.start", &json!({ "scope": "vault" }));
|
|
drain(®istry, &mut session);
|
|
let decided = run(®istry, &mut session, "batch.decide", &json!({ "all": true, "decision": "accept" }));
|
|
assert_eq!(decided["decided"], json!(2), "the two proposed notes, not the needs-review one");
|
|
|
|
let applied = run(®istry, &mut session, "batch.apply", &Value::Null);
|
|
assert_eq!(applied["written"], json!(2));
|
|
assert_eq!(on_disk(vault.path(), "json.md"), JSON_FORMATTED);
|
|
assert_eq!(on_disk(vault.path(), "lint.md"), LINT_FIXED);
|
|
assert_eq!(on_disk(vault.path(), "unlabeled.md"), UNLABELED_NOTE, "needs review waits for a per-change decision");
|
|
}
|
|
|
|
#[test]
|
|
fn apply_skips_a_note_edited_after_the_check() {
|
|
let files: &[(&str, &str)] = &[("a.md", JSON_NOTE), ("b.md", JSON_NOTE), ("lint.md", LINT_NOTE)];
|
|
let (registry, mut session, vault) = setup("batch-stale", files);
|
|
run(®istry, &mut session, "batch.start", &json!({ "scope": "vault" }));
|
|
drain(®istry, &mut session);
|
|
for path in ["a.md", "b.md"] {
|
|
run(®istry, &mut session, "batch.decide", &json!({ "path": path, "decision": "accept" }));
|
|
}
|
|
let edited = "# A\n\nedited by hand\n";
|
|
std::fs::write(vault.path().join("a.md"), edited).expect("edit a.md after the check");
|
|
|
|
let applied = run(®istry, &mut session, "batch.apply", &Value::Null);
|
|
assert_eq!(applied["written"], json!(1));
|
|
assert_eq!(applied["skipped"], json!([{ "path": "a.md", "reason": "changed since the check" }]));
|
|
assert_eq!(applied["failed"], json!([]));
|
|
assert_eq!(on_disk(vault.path(), "a.md"), edited, "the fresh edit stays");
|
|
assert_eq!(on_disk(vault.path(), "b.md"), JSON_FORMATTED);
|
|
}
|
|
|
|
#[test]
|
|
fn cancel_empties_the_queue_and_keeps_the_proposals() {
|
|
let (registry, mut session, _vault) = setup("batch-cancel", MAIN_NOTES);
|
|
run(®istry, &mut session, "batch.start", &json!({ "scope": "vault" }));
|
|
let first = run(®istry, &mut session, "batch.step", &json!({ "budget_ms": 1 }));
|
|
let done = first["done"].as_u64().expect("done");
|
|
assert!(done >= 1, "a step always reads at least one note");
|
|
|
|
let cancelled = run(®istry, &mut session, "batch.cancel", &Value::Null);
|
|
assert_eq!(cancelled["done"], json!(done));
|
|
assert_eq!(cancelled["finished"], json!(true));
|
|
assert_eq!(cancelled["cancelled"], json!(true));
|
|
|
|
let after = run(®istry, &mut session, "batch.step", &json!({ "budget_ms": 1 }));
|
|
assert_eq!(after["done"], json!(done), "nothing is read after the cancel");
|
|
let list = run(®istry, &mut session, "batch.proposals", &Value::Null);
|
|
assert_eq!(list["proposals"].as_array().expect("rows").len(), usize::try_from(done).unwrap_or(0));
|
|
}
|
|
|
|
#[test]
|
|
fn scopes_pick_their_notes_and_reserved_folders_are_never_queued() {
|
|
let files: &[(&str, &str)] =
|
|
&[("root.md", JSON_NOTE), ("sub/a.md", JSON_NOTE), ("sub/b.md", CLEAN_NOTE), (".obsidian/x.md", JSON_NOTE), (".notescraft/y.md", JSON_NOTE)];
|
|
let (registry, mut session, _vault) = setup("batch-scopes", files);
|
|
|
|
let vault_scope = run(®istry, &mut session, "batch.start", &json!({ "scope": "vault" }));
|
|
assert_eq!(vault_scope["total"], json!(3), "the reserved folders are never queued");
|
|
|
|
let folder = run(®istry, &mut session, "batch.start", &json!({ "scope": "folder", "path": "sub" }));
|
|
assert_eq!(folder["total"], json!(2));
|
|
let empty = run(®istry, &mut session, "batch.start", &json!({ "scope": "folder", "path": "nope" }));
|
|
assert_eq!(empty["total"], json!(0));
|
|
|
|
let note = run(®istry, &mut session, "batch.start", &json!({ "scope": "note", "path": "sub/a.md" }));
|
|
assert_eq!(note["total"], json!(1));
|
|
let drained = run(®istry, &mut session, "batch.step", &json!({ "budget_ms": 1000 }));
|
|
assert_eq!(drained["done"], json!(1));
|
|
let rows = run(®istry, &mut session, "batch.proposals", &Value::Null);
|
|
assert_eq!(rows["proposals"][0]["path"], json!("sub/a.md"));
|
|
}
|
|
|
|
#[test]
|
|
fn start_is_refused_until_the_opening_scan_ends() {
|
|
let vault = temp_vault("batch-indexing");
|
|
seed(vault.path(), MAIN_NOTES);
|
|
let mut registry = Registry::new();
|
|
register_core(&mut registry).expect("register");
|
|
let mut session = Session::open_lazy(vault.path()).expect("lazy open");
|
|
let refused = registry.run("batch.start", &mut session, &json!({ "scope": "vault" }));
|
|
assert_eq!(code(refused), "indexing");
|
|
while !session.index_progress().finished {
|
|
session.index_step(Duration::from_secs(30));
|
|
}
|
|
let started = run(®istry, &mut session, "batch.start", &json!({ "scope": "vault" }));
|
|
assert_eq!(started["total"], json!(4));
|
|
}
|
|
|
|
#[test]
|
|
fn hostile_params_are_refused_and_change_nothing() {
|
|
let (registry, mut session, vault) = setup("batch-hostile", MAIN_NOTES);
|
|
run(®istry, &mut session, "batch.start", &json!({ "scope": "vault" }));
|
|
|
|
assert_eq!(code(registry.run("batch.start", &mut session, &json!({ "scope": "x" }))), "invalid_params");
|
|
assert_eq!(code(registry.run("batch.start", &mut session, &json!({ "scope": "note" }))), "invalid_params");
|
|
assert_eq!(code(registry.run("batch.start", &mut session, &json!({ "scope": "note", "path": "missing.md" }))), "not_found");
|
|
assert_eq!(
|
|
code(registry.run("batch.start", &mut session, &json!({ "scope": "note", "path": ".obsidian/x.md" }))),
|
|
"not_found",
|
|
"a reserved path is never a batch note"
|
|
);
|
|
assert_eq!(code(registry.run("batch.start", &mut session, &json!({ "scope": "folder" }))), "invalid_params");
|
|
assert_eq!(code(registry.run("batch.step", &mut session, &json!({ "budget_ms": 0 }))), "invalid_params");
|
|
assert_eq!(code(registry.run("batch.step", &mut session, &json!({ "budget_ms": 1_000_000_000 }))), "invalid_params");
|
|
assert_eq!(code(registry.run("batch.step", &mut session, &json!({ "budget_ms": -5 }))), "invalid_params");
|
|
assert_eq!(code(registry.run("batch.proposals", &mut session, &json!({ "status": "bogus" }))), "invalid_params");
|
|
assert_eq!(code(registry.run("batch.proposal", &mut session, &json!({ "path": "missing.md" }))), "not_found");
|
|
assert_eq!(code(registry.run("batch.decide", &mut session, &json!({ "path": "missing.md", "decision": "accept" }))), "not_found");
|
|
assert_eq!(code(registry.run("batch.decide", &mut session, &json!({ "path": "json.md", "change_id": 1_000_000_000, "decision": "accept" }))), "not_found");
|
|
assert_eq!(code(registry.run("batch.decide", &mut session, &json!({ "path": "json.md", "change_id": -1, "decision": "accept" }))), "invalid_params");
|
|
assert_eq!(code(registry.run("batch.decide", &mut session, &json!({ "decision": "weird" }))), "invalid_params");
|
|
assert_eq!(code(registry.run("batch.decide", &mut session, &json!({ "decision": "accept" }))), "invalid_params");
|
|
assert_eq!(code(registry.run("batch.decide", &mut session, &json!({ "all": true, "decision": "skip" }))), "invalid_params");
|
|
assert_eq!(code(registry.run("batch.decide", &mut session, &json!({ "all": true, "decision": "accept", "change_id": 3 }))), "invalid_params");
|
|
|
|
let progress = run(®istry, &mut session, "batch.status", &Value::Null);
|
|
assert_eq!(progress["done"], json!(0), "no hostile call touched the batch");
|
|
for (path, content) in MAIN_NOTES {
|
|
assert_eq!(on_disk(vault.path(), path), *content);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn unreadable_and_oversized_notes_are_reported_not_edited() {
|
|
let huge = "x".repeat(3 * 1024 * 1024);
|
|
let files: Vec<(&str, &str)> = vec![("gone.md", CLEAN_NOTE), ("huge.md", huge.as_str()), ("ok.md", JSON_NOTE)];
|
|
let (registry, mut session, vault) = setup("batch-refused", &files);
|
|
run(®istry, &mut session, "batch.start", &json!({ "scope": "vault" }));
|
|
std::fs::remove_file(vault.path().join("gone.md")).expect("delete a note after the start");
|
|
let progress = drain(®istry, &mut session);
|
|
assert_eq!(progress["not_edited"], json!(2));
|
|
assert_eq!(progress["proposed"], json!(1), "ok.md still reviews fine");
|
|
|
|
let list = run(®istry, &mut session, "batch.proposals", &json!({ "status": "not_edited" }));
|
|
let rows = list["proposals"].as_array().expect("rows").clone();
|
|
assert_eq!(rows.len(), 2);
|
|
let reason = |path: &str| row(&rows, path)["reason"].as_str().map(str::to_owned).unwrap_or_default();
|
|
assert!(reason("gone.md").contains("read"), "{}", reason("gone.md"));
|
|
assert!(reason("huge.md").contains("2 MiB"), "{}", reason("huge.md"));
|
|
}
|
|
|
|
#[test]
|
|
fn a_session_with_lint_off_reports_lint_notes_as_not_edited() {
|
|
let vault = temp_vault("batch-lint-off");
|
|
seed(vault.path(), MAIN_NOTES);
|
|
let mut config = SessionConfig::default();
|
|
config.lint.enabled = false;
|
|
let mut registry = Registry::new();
|
|
register_core(&mut registry).expect("register");
|
|
let mut session = Session::open_with(vault.path(), config).expect("open");
|
|
|
|
// a batch that asks for lint while lint is off reports every note as not edited
|
|
run(®istry, &mut session, "batch.start", &json!({ "scope": "vault" }));
|
|
let progress = drain(®istry, &mut session);
|
|
assert_eq!(progress["not_edited"], json!(4));
|
|
assert_eq!(progress["proposed"], json!(0));
|
|
|
|
// with the lint check off the batch still reviews JSON
|
|
run(®istry, &mut session, "batch.start", &json!({ "scope": "vault", "lint": false }));
|
|
let progress = drain(®istry, &mut session);
|
|
assert_eq!(progress["proposed"], json!(1), "json.md only: no lint fixes were requested");
|
|
assert_eq!(progress["needs_review"], json!(1));
|
|
assert_eq!(progress["not_edited"], json!(0));
|
|
}
|
|
|
|
/// An unlabeled Rust fence: the label change is a zero-length insertion right after the fence run.
|
|
const RUST_NOTE: &str = "# R\n\n```\nfn main() {\n let x = 1;\n}\n```\n";
|
|
/// The same note with the detected label inserted.
|
|
const RUST_LABELED: &str = "# R\n\n```rust\nfn main() {\n let x = 1;\n}\n```\n";
|
|
/// An unlabeled Python fence.
|
|
const PYTHON_NOTE: &str = "```\ndef add(a, b):\n return a + b\n```\n";
|
|
/// A fence that already carries its label.
|
|
const LABELED_NOTE: &str = "```rust\nfn main() {}\n```\n";
|
|
|
|
#[test]
|
|
fn unlabeled_code_blocks_get_label_changes_and_apply_inserts_the_label() {
|
|
let files: &[(&str, &str)] = &[("rust.md", RUST_NOTE), ("python.md", PYTHON_NOTE), ("labeled.md", LABELED_NOTE)];
|
|
let (registry, mut session, vault) = setup("batch-labels", files);
|
|
|
|
let started = run(®istry, &mut session, "batch.start", &json!({ "scope": "vault" }));
|
|
assert_eq!(started, json!({ "total": 3 }));
|
|
let progress = drain(®istry, &mut session);
|
|
assert_eq!(progress["needs_review"], json!(2), "rust.md and python.md guess their language");
|
|
assert_eq!(progress["clean"], json!(1), "labeled.md already has its label");
|
|
|
|
let full = run(®istry, &mut session, "batch.proposal", &json!({ "path": "rust.md" }));
|
|
assert_eq!(full["status"], json!("NeedsReview"));
|
|
let change = &full["changes"][0];
|
|
assert_eq!(change["kind"], json!("Label"));
|
|
assert_eq!(change["before"], json!(""));
|
|
assert_eq!(change["after"], json!("rust"));
|
|
assert_eq!(change["safe"], json!(false));
|
|
let fence = RUST_NOTE.find("```\n").expect("the opening fence line") + "```".len();
|
|
assert_eq!(change["range"], json!({ "start": fence, "end": fence }));
|
|
assert_eq!(change["line"], json!(3), "the insertion sits on the fence line");
|
|
|
|
run(®istry, &mut session, "batch.decide", &json!({ "path": "python.md", "decision": "accept" }));
|
|
let applied = run(®istry, &mut session, "batch.apply", &Value::Null);
|
|
assert_eq!(applied["written"], json!(1));
|
|
assert_eq!(on_disk(vault.path(), "python.md"), "```python\ndef add(a, b):\n return a + b\n```\n");
|
|
assert_eq!(on_disk(vault.path(), "rust.md"), RUST_NOTE, "pending is not applied");
|
|
assert_eq!(on_disk(vault.path(), "labeled.md"), LABELED_NOTE, "a labeled fence is never relabeled");
|
|
}
|
|
|
|
#[test]
|
|
fn labels_false_proposes_nothing_and_an_open_buffer_gets_one_undo_step() {
|
|
let files: &[(&str, &str)] = &[("rust.md", RUST_NOTE), ("labeled.md", LABELED_NOTE)];
|
|
let (registry, mut session, _vault) = setup("batch-labels-off", files);
|
|
|
|
run(®istry, &mut session, "batch.start", &json!({ "scope": "vault", "labels": false }));
|
|
let progress = drain(®istry, &mut session);
|
|
assert_eq!(progress["clean"], json!(2), "with the label check off nothing is guessed");
|
|
assert_eq!(progress["needs_review"], json!(0));
|
|
assert_eq!(progress["proposed"], json!(0));
|
|
|
|
// the same note, labels on, applied to an open buffer: ONE undo step takes it back
|
|
run(®istry, &mut session, "buffer.open", &json!({ "path": "rust.md" }));
|
|
run(®istry, &mut session, "batch.start", &json!({ "scope": "vault", "labels": true }));
|
|
drain(®istry, &mut session);
|
|
run(®istry, &mut session, "batch.decide", &json!({ "path": "rust.md", "decision": "accept" }));
|
|
let applied = run(®istry, &mut session, "batch.apply", &Value::Null);
|
|
assert_eq!(applied["written"], json!(1));
|
|
let opened = run(®istry, &mut session, "buffer.open", &json!({ "path": "rust.md" }));
|
|
assert_eq!(opened["text"], json!(RUST_LABELED));
|
|
let undone = run(®istry, &mut session, "buffer.undo", &json!({ "path": "rust.md" }));
|
|
assert_eq!(undone["undone"], json!(true), "one undo step covers the whole apply");
|
|
let back = run(®istry, &mut session, "buffer.open", &json!({ "path": "rust.md" }));
|
|
assert_eq!(back["text"], json!(RUST_NOTE));
|
|
}
|