authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-11 04:01:57-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-12 17:00:32-07:00
log57e5817759454103af57785fe4a566b34a18c1a7
tree6173f84111020c2de541cc3b2791113f30f211b1
parenta380cb56a0a524f24a9edfa7a153336e4db3201c
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

test: retain recovery evidence across remote restores

Exercise empty, unsent, and uncertain queues against an older remote image across cache reopen. Verify that historical receipts do not trigger replay and recovery archives retain the original branch. Assisted-by: gpt-6-astra

2 files changed, 93 insertions(+), 1 deletions(-)

crates/notebook/src/sync.rs+1-1
...@@ -35,7 +35,7 @@ pub enum EditStatus {...@@ -35,7 +35,7 @@ pub enum EditStatus {
35 revision: ExGuid,35 revision: ExGuid,
36 },36 },
37 Conflict(ConflictKind),37 Conflict(ConflictKind),
38 /// Revision of the intent's space when its complete effect was confirmed.38 /// Historical confirmation; later remote edits or restores may remove the effect.
39 Published {39 Published {
40 revision: ExGuid,40 revision: ExGuid,
41 },41 },
crates/notebook/tests/sync.rs+92
...@@ -825,3 +825,95 @@ fn remote_changes_after_review_cannot_be_overwritten_by_the_reviewed_page() {...@@ -825,3 +825,95 @@ fn remote_changes_after_review_cannot_be_overwritten_by_the_reviewed_page() {
825 );825 );
826 }826 }
827}827}
828
829#[test]
830fn remote_restore_retains_historical_receipts_without_replaying_them() {
831 let directory = tempfile::tempdir().unwrap();
832 let path = directory.path().join("cache.sqlite");
833 let source = onestore::create_section("restore.one", "Original", "Fixture").unwrap();
834 let (_, object, _) = text(&source);
835 let cache = Replica::create(&path, &source).unwrap();
836 let mut server = Server::new(&source);
837 let published = save(&cache, object, 0..0, "Published ").unwrap();
838 let (_, receipt) = cache.sync_once(&mut server).unwrap().unwrap();
839 assert!(matches!(receipt, EditStatus::Published { .. }));
840 let published_image = cache.snapshot().unwrap();
841 cache
842 .export_recovery(directory.path().join("published.sqlite"))
843 .unwrap();
844 drop(cache);
845
846 server.visible.clone_from(&source);
847 server.durable.clone_from(&source);
848 let cache = Replica::open(&path).unwrap();
849 assert_eq!(cache.sync_once(&mut server).unwrap(), None);
850 assert_eq!(cache.snapshot().unwrap(), source);
851 assert_eq!(cache.remote_snapshot().unwrap(), source);
852 assert_eq!(cache.status(published).unwrap(), Some(receipt));
853 assert_eq!(server.publications, 1);
854 assert!(cache.pending().unwrap().is_empty());
855 let archive = notebook::Recovery::open(directory.path().join("published.sqlite")).unwrap();
856 assert_eq!(archive.snapshot().unwrap(), published_image);
857 assert_eq!(archive.status(published).unwrap(), Some(receipt));
858 assert_eq!(text(&archive.snapshot().unwrap()).2, "Published Original");
859 drop(cache);
860 let cache = Replica::open(path).unwrap();
861 assert_eq!(text(&cache.snapshot().unwrap()).2, "Original");
862 assert_eq!(cache.status(published).unwrap(), Some(receipt));
863}
864
865#[test]
866fn remote_restore_rebases_unsent_work_but_never_replays_an_uncertain_attempt() {
867 for fault in [Fault::None, Fault::UnknownBefore, Fault::UnknownAfter] {
868 let uncertain = !matches!(fault, Fault::None);
869 let directory = tempfile::tempdir().unwrap();
870 let path = directory.path().join("cache.sqlite");
871 let source = onestore::create_section("restore.one", "Original", "Fixture").unwrap();
872 let (_, object, _) = text(&source);
873 let cache = Replica::create(&path, &source).unwrap();
874 let mut server = Server::new(&source);
875 let published = save(&cache, object, 0..0, "Published ").unwrap();
876 let (_, receipt) = cache.sync_once(&mut server).unwrap().unwrap();
877 let end = u32::try_from(text(&cache.snapshot().unwrap()).2.encode_utf16().count()).unwrap();
878 let queued = save(&cache, object, end..end, " Local").unwrap();
879 if uncertain {
880 server.fault = fault;
881 assert!(cache.sync_once(&mut server).is_err());
882 }
883 let prior_status = cache.status(queued).unwrap();
884 let local = cache.snapshot().unwrap();
885 let pending = cache.pending().unwrap();
886 cache
887 .export_recovery(directory.path().join("before-restore.sqlite"))
888 .unwrap();
889 drop(cache);
890 server.visible.clone_from(&source);
891 server.durable.clone_from(&source);
892 let attempts = server.publications;
893 let cache = Replica::open(&path).unwrap();
894 let (_, status) = cache.sync_once(&mut server).unwrap().unwrap();
895 assert_eq!(cache.status(published).unwrap(), Some(receipt));
896 if uncertain {
897 assert!(matches!(status, EditStatus::AwaitingConfirmation { .. }));
898 assert_eq!(Some(status), prior_status);
899 assert_eq!(server.publications, attempts);
900 assert_eq!(server.visible, source);
901 assert_eq!(cache.snapshot().unwrap(), local);
902 assert_eq!(cache.pending().unwrap(), pending);
903 } else {
904 assert!(matches!(status, EditStatus::Published { .. }));
905 assert_eq!(server.publications, attempts + 1);
906 assert_eq!(text(&server.visible).2, "Original Local");
907 assert!(cache.pending().unwrap().is_empty());
908 }
909 let archive =
910 notebook::Recovery::open(directory.path().join("before-restore.sqlite")).unwrap();
911 assert_eq!(archive.snapshot().unwrap(), local);
912 assert_eq!(archive.pending().unwrap(), pending);
913 assert_eq!(archive.status(queued).unwrap(), prior_status);
914 drop(cache);
915 let cache = Replica::open(&path).unwrap();
916 assert_eq!(cache.status(queued).unwrap(), Some(status));
917 assert_eq!(cache.status(published).unwrap(), Some(receipt));
918 }
919}