From e9d0ff1435aa931d38788bf676968c992e370104 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Sat, 10 Oct 2026 20:04:13 -0700 Subject: [PATCH] test: preserve format invariants around revision attribution Account for the revision metadata author and FILETIME while keeping untouched properties, object identities and historical revisions exact. Build checkpoint fault fixtures from the actual dependency depth, and verify first attribution checkpoints once with same-second precision retained. Assisted-by: gpt-6.1-sol --- crates/onestore/src/op/tests.rs | 24 +++- crates/onestore/tests/edit.rs | 35 +++++- crates/onestore/tests/insertion.rs | 61 +++++++++- crates/onestore/tests/page_movement.rs | 70 +++++++++--- crates/onestore/tests/paragraph.rs | 28 +++++ crates/onestore/tests/support/checkpoint.rs | 6 +- crates/onestore/tests/support/metadata.rs | 119 ++++++++++++++++++++ 7 files changed, 316 insertions(+), 27 deletions(-) create mode 100644 crates/onestore/tests/support/metadata.rs diff --git a/crates/onestore/src/op/tests.rs b/crates/onestore/src/op/tests.rs index 53e45e240d3703cfb3316e9bf5903e8a805412ff..ca15018f7f2ca8274348816dcc4f528e484d49bc 100644 --- a/crates/onestore/src/op/tests.rs +++ b/crates/onestore/src/op/tests.rs @@ -2766,7 +2766,7 @@ fn a_page_date_stores_what_onenote_stores() { .apply( "Author", &Edit { - at: AT, + at: AT + 12345, ops: vec![Op::Page { space: kept, op }], }, ) @@ -2777,12 +2777,24 @@ fn a_page_date_stores_what_onenote_stores() { assert_eq!(stored.created, Some(DATED_AT)); assert_eq!(date_fields(&stored), fields); assert_eq!(stored, predicted); - assert_eq!(changed_jcids(&image, kept), native); + let mut expected = native; + expected.extend([0x20044, 0x120001]); + expected.sort(); + assert_eq!(changed_jcids(&image, kept), expected); let store = Store::parse(&image).unwrap(); - RevisionIndex::parse(&store) - .unwrap() - .validate_current() - .unwrap(); + let index = RevisionIndex::parse(&store).unwrap(); + index.validate_current().unwrap(); + let document = Document::parse(&index).unwrap(); + let revision = document.active(kept).unwrap(); + let metadata = &revision.nodes[&revision.roots[&4]]; + assert!(matches!( + metadata.kind, + Kind::RevisionMetadata { modified_filetime: Some(at) } if at == AT + 12345 + )); + assert!(matches!( + &revision.nodes[&metadata.latest_author.unwrap()].kind, + Kind::Author { name } if name.as_deref() == Some("Author") + )); // The whole-page lowering reaches the same page through the same op. let mut after = before.clone(); diff --git a/crates/onestore/tests/edit.rs b/crates/onestore/tests/edit.rs index 9cb6637e4c0c8ec74d373dba7790edb66532e292..19cde8b002f8867ec66f67c38b11c713df6efbcc 100644 --- a/crates/onestore/tests/edit.rs +++ b/crates/onestore/tests/edit.rs @@ -2,6 +2,8 @@ mod checkpoint; #[path = "support/disk.rs"] mod disk; +#[path = "support/metadata.rs"] +mod metadata; #[path = "support/ops.rs"] mod ops; #[path = "support/trace.rs"] @@ -321,10 +323,24 @@ fn assert_other_objects_preserved( .find(|field| field.id == 0x14001d7a) .unwrap() .value; + let filetime = metadata::check(before, after, "Author"); + let author = metadata::author(&after.objects[&after.roots[&4]]); + assert_eq!( + *modified, + onestore::Value::Bytes( + &u32::try_from(filetime / 10_000_000 - 11_960_006_400) + .unwrap() + .to_le_bytes() + ) + ); let reachable = before.reachable().unwrap(); - assert_eq!(before.objects.len(), after.objects.len()); + assert!( + after.objects.keys().all(|id| { + before.objects.contains_key(id) || *id == after.roots[&4] || *id == author + }) + ); for (id, object) in &before.objects { - if *id == edited { + if *id == edited || Some(id) == before.roots.get(&4) { continue; } let current = &after.objects[id]; @@ -334,7 +350,8 @@ fn assert_other_objects_preserved( current .global_ids .iter() - .all(|(entry, guid)| object.global_ids.get(entry) == Some(guid)) + .all(|(entry, guid)| object.global_ids.get(entry) == Some(guid) + || *guid == author.guid) ); let mut pending = vec![*id]; let mut visited = std::collections::BTreeSet::new(); @@ -349,6 +366,10 @@ fn assert_other_objects_preserved( if ancestor { allowed.push(0x14001d7a); } + if ancestor && object.jcid == 0x6000d { + allowed.push(0x20001d79); + assert_eq!(metadata::author(current), author); + } if ancestor && object.jcid == 0x6000b { allowed.push(0x1c001d3c); } @@ -368,7 +389,11 @@ fn assert_other_objects_preserved( if ancestor { let old_time = old.sets[0].iter().find(|field| field.id == 0x14001d7a); let new_time = new.sets[0].iter().find(|field| field.id == 0x14001d7a); - assert_eq!(old_time.is_some(), new_time.is_some()); + if object.jcid == 0x6000d { + assert!(new_time.is_some()); + } else { + assert_eq!(old_time.is_some(), new_time.is_some()); + } if let Some(time) = new_time { assert_eq!(&time.value, modified); } @@ -413,10 +438,10 @@ fn text_splices_preserve_formats_objects_and_history_in_one_transaction() { index.spaces[space_id].labels[&(ExGuid::default(), 1)], ) .unwrap(); - assert_eq!(previous.objects.len(), current.objects.len()); if *space_id == sid { assert_other_objects_preserved(&previous, ¤t, oid); } else { + assert_eq!(previous.objects.len(), current.objects.len()); for (id, object) in previous.objects { assert_eq!(object.data, current.objects[&id].data); } diff --git a/crates/onestore/tests/insertion.rs b/crates/onestore/tests/insertion.rs index 3850c394cf436496f19ad99e737374fecee65ae0..e3ab5406e0fafb25759b9cc9fc495120bd2713b4 100644 --- a/crates/onestore/tests/insertion.rs +++ b/crates/onestore/tests/insertion.rs @@ -4,6 +4,8 @@ mod checkpoint; mod current; #[path = "support/disk.rs"] mod disk; +#[path = "support/metadata.rs"] +mod metadata; #[path = "support/ops.rs"] mod ops; #[path = "support/sweep.rs"] @@ -415,6 +417,60 @@ fn paragraph_and_outline_insertions_publish_metadata_and_references_together() { } } +#[test] +fn first_revision_metadata_checkpoints_once_and_keeps_history() { + let source = onestore::create_section("metadata.one", "Original", "Same author").unwrap(); + let (sid, _, _, _, text) = targets(&source); + let store = Store::parse(&source).unwrap(); + let index = RevisionIndex::parse(&store).unwrap(); + let original = index.resolve_active(sid).unwrap(); + assert!(!original.roots.contains_key(&4)); + let arena = onestore::Arena::default(); + let mut section = onestore::Section::open(&arena, source.clone()).unwrap(); + let at = 134_000_000_000_012_345; + let mut previous = None; + for at in [at, at + 1] { + section + .apply( + "Same author", + &Edit { + at, + ops: vec![Op::Page { + space: sid, + op: PageOp::Text { + text, + range: 0..0, + with: "x".into(), + }, + }], + }, + ) + .unwrap(); + section.seal().unwrap().unwrap(); + let image = section.image(); + let current_store = Store::parse(&image).unwrap(); + let current = RevisionIndex::parse(¤t_store).unwrap(); + current.validate_current().unwrap(); + let rid = current.active(sid).unwrap(); + assert_eq!(current.spaces[&sid].revisions[&rid].dependency, previous); + let revision = current.resolve_active(sid).unwrap(); + assert_eq!(metadata::check(&original, &revision, "Same author"), at); + assert!( + revision + .objects + .keys() + .all(|id| original.objects.contains_key(id) || *id == revision.roots[&4]) + ); + for old in index.spaces[&sid].revisions.keys() { + assert_eq!( + format!("{:?}", index.resolve(sid, *old).unwrap()), + format!("{:?}", current.resolve(sid, *old).unwrap()) + ); + } + previous = Some(rid); + } +} + #[test] fn repeated_insertions_and_formatting_share_immutable_objects() { let mut source = onestore::create_section("shared.one", "Original", "Same author").unwrap(); @@ -468,7 +524,10 @@ fn repeated_insertions_and_formatting_share_immutable_objects() { counts.push((object.jcid, object.reference_count)); } counts.sort(); - assert_eq!(counts, [(0x120001, 26), (0x12004d, 12), (0x12004d, 25)]); + metadata::check(&raw, &raw, "Same author"); + let author = metadata::author(&raw.objects[&raw.roots[&4]]); + assert_eq!(raw.objects[&author].jcid, 0x120001); + assert_eq!(counts, [(0x120001, 27), (0x12004d, 12), (0x12004d, 25)]); let document = Document::parse(&index).unwrap(); let space = &document.spaces[&sid]; let view = &space.revisions[&space.contexts[&ExGuid::default()]]; diff --git a/crates/onestore/tests/page_movement.rs b/crates/onestore/tests/page_movement.rs index d4723ae5bdd06f5957f88e5e4b0d362d27201d0b..6a30a4c189aa18946d7193c5e65eb2c075b6c196 100644 --- a/crates/onestore/tests/page_movement.rs +++ b/crates/onestore/tests/page_movement.rs @@ -11,6 +11,8 @@ use onestore::{ mod current; #[path = "support/disk.rs"] mod disk; +#[path = "support/metadata.rs"] +mod metadata; #[path = "support/sweep.rs"] mod sweep; @@ -91,14 +93,33 @@ fn verify(before: &[u8], after: &[u8], native: &[u8]) { let new = indexes[1] .resolve(*sid, indexes[1].spaces[sid].labels[&(ExGuid::default(), 1)]) .unwrap(); - assert_eq!(old.roots, new.roots); - assert_eq!( - old.objects.keys().collect::>(), - new.objects.keys().collect::>() - ); + let changed = old.objects[&old.roots[&2]].data != new.objects[&new.roots[&2]].data; + let mut authors = std::collections::BTreeSet::new(); + if changed { + metadata::check(&old, &new, "Author"); + authors.insert(metadata::author(&new.objects[&new.roots[&4]])); + if let Some(id) = old.roots.get(&4) { + authors.extend(old.objects[id].references().unwrap().objects); + } + assert!(new.objects.keys().all(|id| { + old.objects.contains_key(id) || *id == new.roots[&4] || authors.contains(id) + })); + } else { + assert_eq!(old.roots, new.roots); + assert_eq!( + old.objects.keys().collect::>(), + new.objects.keys().collect::>() + ); + } for (id, object) in &old.objects { - if *id != old.roots[&2] { - assert_eq!(format!("{object:?}"), format!("{:?}", new.objects[id])); + if *id != old.roots[&2] && !(changed && Some(id) == old.roots.get(&4)) { + let current = &new.objects[id]; + assert_eq!(object.data, current.data); + assert_eq!(object.jcid, current.jcid); + assert_eq!(object.global_ids, current.global_ids); + if !authors.contains(id) { + assert_eq!(object.reference_count, current.reference_count); + } } } } @@ -383,13 +404,34 @@ fn metadata_copy_order_does_not_change_page_identity() { let stores = [edited.as_bytes(), expected.as_slice()].map(|b| Store::parse(b).unwrap()); let indexes = stores.each_ref().map(|s| RevisionIndex::parse(s).unwrap()); let documents = indexes.each_ref().map(|i| Document::parse(i).unwrap()); - for sid in documents[0].spaces.keys() { - let nodes = documents.each_ref().map(|d| { - let space = &d.spaces[sid]; - serde_json::to_value(&space.revisions[&space.contexts[&ExGuid::default()]].nodes) - .unwrap() - }); - assert_eq!(nodes[0], nodes[1], "{sid}"); + for space_id in documents[0].spaces.keys() { + let views = documents + .each_ref() + .map(|d| d.spaces[space_id].active().unwrap()); + let mut nodes = views.map(|view| view.nodes.clone()); + if *space_id == sid { + let raw = indexes.each_ref().map(|index| { + index + .resolve( + *space_id, + index.spaces[space_id].labels[&(ExGuid::default(), 1)], + ) + .unwrap() + }); + metadata::check(&raw[1], &raw[0], "Author"); + let author = metadata::author(&raw[0].objects[&raw[0].roots[&4]]); + if !nodes[1].contains_key(&author) { + nodes[0].remove(&author).unwrap(); + } + for (nodes, view) in nodes.iter_mut().zip(views) { + nodes.remove(&view.roots[&4]).unwrap(); + } + } + assert_eq!( + serde_json::to_value(&nodes[0]).unwrap(), + serde_json::to_value(&nodes[1]).unwrap(), + "{space_id}" + ); } } diff --git a/crates/onestore/tests/paragraph.rs b/crates/onestore/tests/paragraph.rs index 6675166893610afada9c2d7c8fbe71d4e8e401e8..d0f62ba8d82fd789ed64c31cbefb98610089d3ed 100644 --- a/crates/onestore/tests/paragraph.rs +++ b/crates/onestore/tests/paragraph.rs @@ -12,6 +12,8 @@ mod checkpoint; mod current; #[path = "support/disk.rs"] mod disk; +#[path = "support/metadata.rs"] +mod metadata; #[path = "support/ops.rs"] mod ops; @@ -220,6 +222,22 @@ fn joins_match_native_graphs_tags_and_inherited_character_styles() { .unwrap(); let current_rid = current_document.spaces[sid].contexts[&ExGuid::default()]; let new_raw = current_index.resolve(*sid, current_rid).unwrap(); + let modified = metadata::check(&old_raw, &new_raw, "Author"); + let author = metadata::author(&new_raw.objects[&new_raw.roots[&4]]); + changed.extend(old_raw.roots.get(&4)); + for id in &changed { + if current + .nodes + .get(id) + .is_some_and(|node| matches!(node.kind, Kind::Paragraph { .. })) + { + assert_eq!(metadata::author(&new_raw.objects[id]), author); + assert_eq!( + current.nodes[id].modified.map(u64::from), + Some(modified / 10_000_000 - 11_960_006_400) + ); + } + } for (id, object) in old_raw.objects { if !changed.contains(&id) { assert_eq!( @@ -409,6 +427,16 @@ fn splits_partition_native_paragraphs_at_every_scalar_boundary() { let new_raw = current_index .resolve(*sid, current_space.contexts[&ExGuid::default()]) .unwrap(); + let modified = metadata::check(&old_raw, &new_raw, "Author"); + let author = metadata::author(&new_raw.objects[&new_raw.roots[&4]]); + changed.extend(old_raw.roots.get(&4)); + for id in [paragraph, new_paragraph] { + assert_eq!(metadata::author(&new_raw.objects[&id]), author); + assert_eq!( + after.nodes[&id].modified.map(u64::from), + Some(modified / 10_000_000 - 11_960_006_400) + ); + } for (id, object) in old_raw.objects { if !changed.contains(&id) { assert_eq!(object.data, new_raw.objects[&id].data, "untouched {id}"); diff --git a/crates/onestore/tests/support/checkpoint.rs b/crates/onestore/tests/support/checkpoint.rs index 03ccf9f8e8d770536dfe2cb2ff2ea81d7d50d137..6047c9ed4e10e376ae8f757ff1cc51605c0e63e9 100644 --- a/crates/onestore/tests/support/checkpoint.rs +++ b/crates/onestore/tests/support/checkpoint.rs @@ -26,7 +26,11 @@ pub fn pending(source: &[u8], space: ExGuid, text: ExGuid) -> Vec { with: with.into(), }, }; - for at in 0..512 - depth(source) as u64 { + for at in 0..=512 { + let image = section.image(); + if depth(&image) == 512 { + return image; + } let ops = vec![retouch(0..0, "x"), retouch(0..1, "")]; let at = 134_000_000_000_000_000 + at * 10_000_000; section.apply("Author", &Edit { at, ops }).unwrap(); diff --git a/crates/onestore/tests/support/metadata.rs b/crates/onestore/tests/support/metadata.rs new file mode 100644 index 0000000000000000000000000000000000000000..df8bd46e1a77f781881ed9cc9b735927292431e1 --- /dev/null +++ b/crates/onestore/tests/support/metadata.rs @@ -0,0 +1,119 @@ +use onestore::{ExGuid, IdStream, Object, ObjectData, PropertySets, ResolvedRevision, Value}; + +pub fn author(object: &Object<'_>) -> ExGuid { + let ObjectData::Properties(bytes) = object.data else { + panic!("AuthorMostRecent needs a property object") + }; + let fields = PropertySets::parse(bytes).unwrap(); + let Value::References { + stream: IdStream::Objects, + compact_ids, + } = fields.sets[0] + .iter() + .find(|field| field.id == 0x20001d79) + .unwrap() + .value + else { + panic!("AuthorMostRecent needs one object reference") + }; + let compact = u32::from_le_bytes(compact_ids.try_into().unwrap()); + ExGuid { + guid: object.global_ids[&(compact >> 8)], + n: compact & 255, + } +} + +/// Checks the revision context's author/time while preserving every other metadata field. +pub fn check(before: &ResolvedRevision<'_>, after: &ResolvedRevision<'_>, name: &str) -> u64 { + let roots = |revision: &ResolvedRevision<'_>| { + revision + .roots + .iter() + .filter(|(kind, _)| **kind != 4) + .map(|(kind, id)| (*kind, *id)) + .collect::>() + }; + assert_eq!(roots(before), roots(after)); + let id = after.roots[&4]; + let metadata = &after.objects[&id]; + assert_eq!(metadata.jcid, 0x20044); + let ObjectData::Properties(bytes) = metadata.data else { + panic!() + }; + let fields = PropertySets::parse(bytes).unwrap(); + let Value::Bytes(time) = fields.sets[0] + .iter() + .find(|field| field.id == 0x18001d77) + .unwrap() + .value + else { + panic!() + }; + let filetime = u64::from_le_bytes(time.try_into().unwrap()); + assert!(filetime >= 119_600_064_000_000_000); + let author_id = author(metadata); + let named = &after.objects[&author_id]; + assert_eq!(named.jcid, 0x120001); + let ObjectData::Properties(bytes) = named.data else { + panic!() + }; + let named_fields = PropertySets::parse(bytes).unwrap(); + let Value::Bytes(stored) = named_fields.sets[0] + .iter() + .find(|field| field.id == 0x1c001d75) + .unwrap() + .value + else { + panic!() + }; + let expected: Vec<_> = name + .encode_utf16() + .chain([0]) + .flat_map(u16::to_le_bytes) + .collect(); + assert_eq!(stored, expected); + if let Some(old) = before.objects.get(&author_id) { + assert_eq!(old.data, named.data); + } + if let Some(old_id) = before.roots.get(&4) { + assert_eq!(*old_id, id); + let old = &before.objects[old_id]; + assert_eq!(old.jcid, metadata.jcid); + assert!(metadata.global_ids.iter().all(|(entry, guid)| { + old.global_ids.get(entry) == Some(guid) || *guid == author_id.guid + })); + let ObjectData::Properties(old_bytes) = old.data else { + panic!() + }; + let mut old_fields = PropertySets::parse(old_bytes).unwrap(); + let mut new = fields; + for fields in [&mut old_fields, &mut new] { + fields.sets[0].retain(|field| ![0x18001d77, 0x20001d79].contains(&field.id)); + } + assert_eq!(old_fields.sets, new.sets); + for field in old_fields.sets.iter().flatten() { + if let Value::References { compact_ids, .. } = field.value { + let ids = compact_ids.chunks_exact(4); + assert!(ids.remainder().is_empty()); + for compact in ids { + let entry = u32::from_le_bytes(compact.try_into().unwrap()) >> 8; + assert_eq!( + metadata.global_ids.get(&entry), + Some(&old.global_ids[&entry]) + ); + } + } + } + } else { + assert!(!before.objects.contains_key(&id)); + assert_eq!(fields.sets.len(), 1); + assert_eq!( + fields.sets[0] + .iter() + .map(|field| field.id) + .collect::>(), + [0x18001d77, 0x20001d79] + ); + } + filetime +} -- 2.54.0