From de3ecb8964829cbf26e3abd78486bc59405b2ac4 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Sat, 10 Oct 2026 20:06:47 -0700 Subject: [PATCH] fix(notebook): list an unlisted section when renaming it A folder of section files can have no table of contents. Renaming one created an empty TOC and tried to rename an entry absent from it, leaving the file moved but the application catalog unchanged. Add an entry when that identity is unlisted, retaining Rename for existing entries. Cover two discovered sections without a TOC, their file identities, current catalog and cold reopen from a fresh cache. The fixture can export through SNOWBOUND_RENAME_EXPORT for a native cold-open check. Fixes #109 Assisted-by: gpt-6.1-sol --- crates/notebook/src/session.rs | 17 ++++++--- crates/notebook/src/session/tests.rs | 54 ++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/crates/notebook/src/session.rs b/crates/notebook/src/session.rs index cee67f696a786d7eba414ff01ec6da746bdd34e3..ad7371f978f0a60d002f66cf3f1622d1b489f1ee 100644 --- a/crates/notebook/src/session.rs +++ b/crates/notebook/src/session.rs @@ -817,13 +817,20 @@ impl Notebook { self.storage.place(&placed, ancestor, &target)?; } if let Some(identity) = identity { - self.edit_toc( - folder, - &[onestore::TocEdit::Rename { + let (toc, _) = self.toc(folder)?; + let edit = if lists(&self.storage.read(&toc)?, identity)? { + onestore::TocEdit::Rename { identity, filename: target, - }], - )?; + } + } else { + onestore::TocEdit::Add { + identity, + filename: target, + group: !filename.to_ascii_lowercase().ends_with(".one"), + } + }; + self.commit_toc(&toc, &[edit])?; } self.refresh()?; Ok(renamed) diff --git a/crates/notebook/src/session/tests.rs b/crates/notebook/src/session/tests.rs index 4cda501114d64f6b1e83eda52cc8bdc1700e3525..e7b57b11c19e252624f3c95d9dcad4dd70e0a33a 100644 --- a/crates/notebook/src/session/tests.rs +++ b/crates/notebook/src/session/tests.rs @@ -19,6 +19,60 @@ fn cached_sections_refuse_links_outside_the_notebook_and_dangling_links() { assert!(notebook.section(path, || {}).is_err()); } +#[test] +fn renaming_an_unlisted_section_creates_its_toc_entry_and_preserves_its_neighbor() { + let temporary = tempfile::tempdir().unwrap(); + let folder = temporary.path().join("notebook"); + let cache = temporary.path().join("cache"); + let page = PageCreation::new(None, Some("Garden plan"), "Fixture").unwrap(); + let mut notebook = Notebook::create(&folder, &cache, Notebook::NEW_COLOR, &page).unwrap(); + let original = notebook.catalog().sections[0].path.clone(); + let other = notebook.create_section("", "Other", &page).unwrap(); + let identity = notebook.section_path(&original).unwrap().file_id; + let neighbor = notebook.section_path(&other).unwrap().file_id; + drop(notebook); + fs::remove_file(folder.join(TOC)).unwrap(); + let mut notebook = Notebook::open(&folder, &cache).unwrap(); + assert!(notebook.catalog().toc.is_none()); + assert_eq!(notebook.catalog().sections.len(), 2); + assert_eq!( + notebook.rename(&original, "Delayed section").unwrap(), + "Delayed section.one" + ); + assert!(!folder.join(&original).exists()); + let check = |notebook: &Notebook| { + assert_eq!(notebook.catalog().sections.len(), 2); + assert!(notebook.section_path(&original).is_err()); + assert_eq!( + notebook + .section_path("Delayed section.one") + .unwrap() + .file_id, + identity + ); + assert_eq!(notebook.section_path(&other).unwrap().file_id, neighbor); + let toc = notebook.read_toc("").unwrap(); + assert!(lists(&toc, identity).unwrap()); + assert_eq!( + stored_pages(¬ebook.read_section("Delayed section.one").unwrap()).unwrap()[0] + .page + .title, + "Garden plan" + ); + }; + check(¬ebook); + drop(notebook); + check(&Notebook::open(&folder, temporary.path().join("cold-cache")).unwrap()); + if let Some(export) = std::env::var_os("SNOWBOUND_RENAME_EXPORT") { + let export = Path::new(&export); + fs::create_dir_all(export).unwrap(); + for entry in fs::read_dir(&folder).unwrap() { + let entry = entry.unwrap(); + fs::copy(entry.path(), export.join(entry.file_name())).unwrap(); + } + } +} + /// A mounted notebook where another writer commits `revision` to the section file just /// before Snowbound's rewritten file takes its place. struct Racing { -- 2.54.0