authorclover caruso <git@paperclover.net> 2026-10-10 20:06:47-07:00
committerclover caruso <git@paperclover.net> 2026-10-10 22:56:05-07:00
logde3ecb8964829cbf26e3abd78486bc59405b2ac4
tree90349addcf092b1df532885758691c7fdd7fc76c
parent1f08968209f50a1148db7425752efaea0af361a9
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover)

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

2 files changed, 66 insertions(+), 5 deletions(-)

crates/notebook/src/session.rs+12-5
...@@ -817,13 +817,20 @@ impl Notebook {...@@ -817,13 +817,20 @@ impl Notebook {
817 self.storage.place(&placed, ancestor, &target)?;817 self.storage.place(&placed, ancestor, &target)?;
818 }818 }
819 if let Some(identity) = identity {819 if let Some(identity) = identity {
820 self.edit_toc(820 let (toc, _) = self.toc(folder)?;
821 folder,821 let edit = if lists(&self.storage.read(&toc)?, identity)? {
822 &[onestore::TocEdit::Rename {822 onestore::TocEdit::Rename {
823 identity,823 identity,
824 filename: target,824 filename: target,
825 }],825 }
826 )?;826 } else {
827 onestore::TocEdit::Add {
828 identity,
829 filename: target,
830 group: !filename.to_ascii_lowercase().ends_with(".one"),
831 }
832 };
833 self.commit_toc(&toc, &[edit])?;
827 }834 }
828 self.refresh()?;835 self.refresh()?;
829 Ok(renamed)836 Ok(renamed)
crates/notebook/src/session/tests.rs+54
...@@ -19,6 +19,60 @@ fn cached_sections_refuse_links_outside_the_notebook_and_dangling_links() {...@@ -19,6 +19,60 @@ fn cached_sections_refuse_links_outside_the_notebook_and_dangling_links() {
19 assert!(notebook.section(path, || {}).is_err());19 assert!(notebook.section(path, || {}).is_err());
20}20}
2121
22#[test]
23fn renaming_an_unlisted_section_creates_its_toc_entry_and_preserves_its_neighbor() {
24 let temporary = tempfile::tempdir().unwrap();
25 let folder = temporary.path().join("notebook");
26 let cache = temporary.path().join("cache");
27 let page = PageCreation::new(None, Some("Garden plan"), "Fixture").unwrap();
28 let mut notebook = Notebook::create(&folder, &cache, Notebook::NEW_COLOR, &page).unwrap();
29 let original = notebook.catalog().sections[0].path.clone();
30 let other = notebook.create_section("", "Other", &page).unwrap();
31 let identity = notebook.section_path(&original).unwrap().file_id;
32 let neighbor = notebook.section_path(&other).unwrap().file_id;
33 drop(notebook);
34 fs::remove_file(folder.join(TOC)).unwrap();
35 let mut notebook = Notebook::open(&folder, &cache).unwrap();
36 assert!(notebook.catalog().toc.is_none());
37 assert_eq!(notebook.catalog().sections.len(), 2);
38 assert_eq!(
39 notebook.rename(&original, "Delayed section").unwrap(),
40 "Delayed section.one"
41 );
42 assert!(!folder.join(&original).exists());
43 let check = |notebook: &Notebook| {
44 assert_eq!(notebook.catalog().sections.len(), 2);
45 assert!(notebook.section_path(&original).is_err());
46 assert_eq!(
47 notebook
48 .section_path("Delayed section.one")
49 .unwrap()
50 .file_id,
51 identity
52 );
53 assert_eq!(notebook.section_path(&other).unwrap().file_id, neighbor);
54 let toc = notebook.read_toc("").unwrap();
55 assert!(lists(&toc, identity).unwrap());
56 assert_eq!(
57 stored_pages(&notebook.read_section("Delayed section.one").unwrap()).unwrap()[0]
58 .page
59 .title,
60 "Garden plan"
61 );
62 };
63 check(&notebook);
64 drop(notebook);
65 check(&Notebook::open(&folder, temporary.path().join("cold-cache")).unwrap());
66 if let Some(export) = std::env::var_os("SNOWBOUND_RENAME_EXPORT") {
67 let export = Path::new(&export);
68 fs::create_dir_all(export).unwrap();
69 for entry in fs::read_dir(&folder).unwrap() {
70 let entry = entry.unwrap();
71 fs::copy(entry.path(), export.join(entry.file_name())).unwrap();
72 }
73 }
74}
75
22/// A mounted notebook where another writer commits `revision` to the section file just76/// A mounted notebook where another writer commits `revision` to the section file just
23/// before Snowbound's rewritten file takes its place.77/// before Snowbound's rewritten file takes its place.
24struct Racing {78struct Racing {