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 {
817817 self.storage.place(&placed, ancestor, &target)?;
818818 }
819819 if let Some(identity) = identity {
820 self.edit_toc(
821 folder,
822 &[onestore::TocEdit::Rename {
820 let (toc, _) = self.toc(folder)?;
821 let edit = if lists(&self.storage.read(&toc)?, identity)? {
822 onestore::TocEdit::Rename {
823823 identity,
824824 filename: target,
825 }],
826 )?;
825 }
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])?;
827834 }
828835 self.refresh()?;
829836 Ok(renamed)
crates/notebook/src/session/tests.rs+54
......@@ -19,6 +19,60 @@ fn cached_sections_refuse_links_outside_the_notebook_and_dangling_links() {
1919 assert!(notebook.section(path, || {}).is_err());
2020}
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
2276/// A mounted notebook where another writer commits `revision` to the section file just
2377/// before Snowbound's rewritten file takes its place.
2478struct Racing {