authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-07 16:55:17-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-07 16:55:17-07:00
logd7d1c96c52f172bdfa45f44531559f14750e55f9
tree54e32469f98454ad0950dd3cbfc877bb6ab23952
parente076b4012533d329f3c9c7aca05c8288d84afb4b
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

test: retain uncertainty when identical text hides different formatting

Reject a retired text confirmation that would confuse deletion of either of two identically spelled characters with different formatting. Verify restart, retained local work, and no confirmation or replay. Validation: all 43 offline synchronization tests; cargo fmt check. A separate native save and cold reopen fixture confirms saved text interoperability. Assisted-by: gpt-6-astra

1 files changed, 51 insertions(+), 0 deletions(-)

crates/onestore-offline/tests/sync.rs+51
......@@ -2075,6 +2075,57 @@ fn retired_format_attempts_require_the_complete_durable_effect_without_replay()
20752075 }
20762076}
20772077
2078#[test]
2079fn retired_text_with_equal_plaintext_but_a_different_surviving_run_remains_uncertain() {
2080 use onestore::TextAttribute;
2081
2082 let directory = tempfile::tempdir().unwrap();
2083 let path = directory.path().join("repeated.sqlite");
2084 let plain = onestore::create_section("repeated.one", "aa", "Fixture").unwrap();
2085 let (space, object, _) = text(&plain);
2086 let source = PreparedEdit::format(&plain, space, object, 0..1, &[TextAttribute::Bold(true)])
2087 .unwrap()
2088 .as_bytes()
2089 .to_vec();
2090 let cache = Replica::create(&path, &source).unwrap();
2091 let id = cache
2092 .edit_text(&source, space, object, 0..1, "")
2093 .unwrap()
2094 .unwrap();
2095 let local = cache.snapshot().unwrap();
2096 let mut server = Server::new(&source);
2097 server.fault = Fault::UnknownAfter;
2098 assert!(cache.sync_once(&mut server).is_err());
2099 let attempt = cache.status(id).unwrap();
2100 server.visible = onestore::replace_text(&source, space, object, 1..2, "").unwrap();
2101 assert_eq!(text(&server.visible).2, text(&local).2);
2102 let bold = |bytes: &[u8]| {
2103 let store = Store::parse(bytes).unwrap();
2104 let index = RevisionIndex::parse(&store).unwrap();
2105 let document = Document::parse(&index).unwrap();
2106 let space = &document.spaces[&space];
2107 space.revisions[&space.contexts[&ExGuid::default()]]
2108 .text_runs(object)
2109 .unwrap()
2110 .into_iter()
2111 .find(|run| !run.text.is_empty())
2112 .unwrap()
2113 .format
2114 .bold
2115 };
2116 assert_eq!(bold(&server.visible), Some(true));
2117 assert_ne!(bold(&server.visible), bold(&local));
2118 drop(cache);
2119 let cache = Replica::open(&path).unwrap();
2120 assert_eq!(
2121 cache.sync_once(&mut server).unwrap(),
2122 attempt.map(|state| (id, state))
2123 );
2124 assert_eq!(server.publications, 1);
2125 assert_eq!(server.confirmations, 0);
2126 assert_eq!(cache.snapshot().unwrap(), local);
2127}
2128
20782129#[test]
20792130fn uncertain_formatting_keeps_the_original_attempt_and_never_replays() {
20802131 use onestore::TextAttribute as A;