authorclover caruso <git@paperclover.net> 2026-10-10 20:04:13-07:00
committerclover caruso <git@paperclover.net> 2026-10-10 22:56:05-07:00
loge9d0ff1435aa931d38788bf676968c992e370104
treedeba8d70fb04411dcddb63f32d46d6253cc8f31a
parentde3ecb8964829cbf26e3abd78486bc59405b2ac4
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover)

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

7 files changed, 316 insertions(+), 27 deletions(-)

crates/onestore/src/op/tests.rs+18-6
......@@ -2766,7 +2766,7 @@ fn a_page_date_stores_what_onenote_stores() {
27662766 .apply(
27672767 "Author",
27682768 &Edit {
2769 at: AT,
2769 at: AT + 12345,
27702770 ops: vec![Op::Page { space: kept, op }],
27712771 },
27722772 )
......@@ -2777,12 +2777,24 @@ fn a_page_date_stores_what_onenote_stores() {
27772777 assert_eq!(stored.created, Some(DATED_AT));
27782778 assert_eq!(date_fields(&stored), fields);
27792779 assert_eq!(stored, predicted);
2780 assert_eq!(changed_jcids(&image, kept), native);
2780 let mut expected = native;
2781 expected.extend([0x20044, 0x120001]);
2782 expected.sort();
2783 assert_eq!(changed_jcids(&image, kept), expected);
27812784 let store = Store::parse(&image).unwrap();
2782 RevisionIndex::parse(&store)
2783 .unwrap()
2784 .validate_current()
2785 .unwrap();
2785 let index = RevisionIndex::parse(&store).unwrap();
2786 index.validate_current().unwrap();
2787 let document = Document::parse(&index).unwrap();
2788 let revision = document.active(kept).unwrap();
2789 let metadata = &revision.nodes[&revision.roots[&4]];
2790 assert!(matches!(
2791 metadata.kind,
2792 Kind::RevisionMetadata { modified_filetime: Some(at) } if at == AT + 12345
2793 ));
2794 assert!(matches!(
2795 &revision.nodes[&metadata.latest_author.unwrap()].kind,
2796 Kind::Author { name } if name.as_deref() == Some("Author")
2797 ));
27862798
27872799 // The whole-page lowering reaches the same page through the same op.
27882800 let mut after = before.clone();
crates/onestore/tests/edit.rs+30-5
......@@ -2,6 +2,8 @@
22mod checkpoint;
33#[path = "support/disk.rs"]
44mod disk;
5#[path = "support/metadata.rs"]
6mod metadata;
57#[path = "support/ops.rs"]
68mod ops;
79#[path = "support/trace.rs"]
......@@ -321,10 +323,24 @@ fn assert_other_objects_preserved(
321323 .find(|field| field.id == 0x14001d7a)
322324 .unwrap()
323325 .value;
326 let filetime = metadata::check(before, after, "Author");
327 let author = metadata::author(&after.objects[&after.roots[&4]]);
328 assert_eq!(
329 *modified,
330 onestore::Value::Bytes(
331 &u32::try_from(filetime / 10_000_000 - 11_960_006_400)
332 .unwrap()
333 .to_le_bytes()
334 )
335 );
324336 let reachable = before.reachable().unwrap();
325 assert_eq!(before.objects.len(), after.objects.len());
337 assert!(
338 after.objects.keys().all(|id| {
339 before.objects.contains_key(id) || *id == after.roots[&4] || *id == author
340 })
341 );
326342 for (id, object) in &before.objects {
327 if *id == edited {
343 if *id == edited || Some(id) == before.roots.get(&4) {
328344 continue;
329345 }
330346 let current = &after.objects[id];
......@@ -334,7 +350,8 @@ fn assert_other_objects_preserved(
334350 current
335351 .global_ids
336352 .iter()
337 .all(|(entry, guid)| object.global_ids.get(entry) == Some(guid))
353 .all(|(entry, guid)| object.global_ids.get(entry) == Some(guid)
354 || *guid == author.guid)
338355 );
339356 let mut pending = vec![*id];
340357 let mut visited = std::collections::BTreeSet::new();
......@@ -349,6 +366,10 @@ fn assert_other_objects_preserved(
349366 if ancestor {
350367 allowed.push(0x14001d7a);
351368 }
369 if ancestor && object.jcid == 0x6000d {
370 allowed.push(0x20001d79);
371 assert_eq!(metadata::author(current), author);
372 }
352373 if ancestor && object.jcid == 0x6000b {
353374 allowed.push(0x1c001d3c);
354375 }
......@@ -368,7 +389,11 @@ fn assert_other_objects_preserved(
368389 if ancestor {
369390 let old_time = old.sets[0].iter().find(|field| field.id == 0x14001d7a);
370391 let new_time = new.sets[0].iter().find(|field| field.id == 0x14001d7a);
371 assert_eq!(old_time.is_some(), new_time.is_some());
392 if object.jcid == 0x6000d {
393 assert!(new_time.is_some());
394 } else {
395 assert_eq!(old_time.is_some(), new_time.is_some());
396 }
372397 if let Some(time) = new_time {
373398 assert_eq!(&time.value, modified);
374399 }
......@@ -413,10 +438,10 @@ fn text_splices_preserve_formats_objects_and_history_in_one_transaction() {
413438 index.spaces[space_id].labels[&(ExGuid::default(), 1)],
414439 )
415440 .unwrap();
416 assert_eq!(previous.objects.len(), current.objects.len());
417441 if *space_id == sid {
418442 assert_other_objects_preserved(&previous, &current, oid);
419443 } else {
444 assert_eq!(previous.objects.len(), current.objects.len());
420445 for (id, object) in previous.objects {
421446 assert_eq!(object.data, current.objects[&id].data);
422447 }
crates/onestore/tests/insertion.rs+60-1
......@@ -4,6 +4,8 @@ mod checkpoint;
44mod current;
55#[path = "support/disk.rs"]
66mod disk;
7#[path = "support/metadata.rs"]
8mod metadata;
79#[path = "support/ops.rs"]
810mod ops;
911#[path = "support/sweep.rs"]
......@@ -415,6 +417,60 @@ fn paragraph_and_outline_insertions_publish_metadata_and_references_together() {
415417 }
416418}
417419
420#[test]
421fn first_revision_metadata_checkpoints_once_and_keeps_history() {
422 let source = onestore::create_section("metadata.one", "Original", "Same author").unwrap();
423 let (sid, _, _, _, text) = targets(&source);
424 let store = Store::parse(&source).unwrap();
425 let index = RevisionIndex::parse(&store).unwrap();
426 let original = index.resolve_active(sid).unwrap();
427 assert!(!original.roots.contains_key(&4));
428 let arena = onestore::Arena::default();
429 let mut section = onestore::Section::open(&arena, source.clone()).unwrap();
430 let at = 134_000_000_000_012_345;
431 let mut previous = None;
432 for at in [at, at + 1] {
433 section
434 .apply(
435 "Same author",
436 &Edit {
437 at,
438 ops: vec![Op::Page {
439 space: sid,
440 op: PageOp::Text {
441 text,
442 range: 0..0,
443 with: "x".into(),
444 },
445 }],
446 },
447 )
448 .unwrap();
449 section.seal().unwrap().unwrap();
450 let image = section.image();
451 let current_store = Store::parse(&image).unwrap();
452 let current = RevisionIndex::parse(&current_store).unwrap();
453 current.validate_current().unwrap();
454 let rid = current.active(sid).unwrap();
455 assert_eq!(current.spaces[&sid].revisions[&rid].dependency, previous);
456 let revision = current.resolve_active(sid).unwrap();
457 assert_eq!(metadata::check(&original, &revision, "Same author"), at);
458 assert!(
459 revision
460 .objects
461 .keys()
462 .all(|id| original.objects.contains_key(id) || *id == revision.roots[&4])
463 );
464 for old in index.spaces[&sid].revisions.keys() {
465 assert_eq!(
466 format!("{:?}", index.resolve(sid, *old).unwrap()),
467 format!("{:?}", current.resolve(sid, *old).unwrap())
468 );
469 }
470 previous = Some(rid);
471 }
472}
473
418474#[test]
419475fn repeated_insertions_and_formatting_share_immutable_objects() {
420476 let mut source = onestore::create_section("shared.one", "Original", "Same author").unwrap();
......@@ -468,7 +524,10 @@ fn repeated_insertions_and_formatting_share_immutable_objects() {
468524 counts.push((object.jcid, object.reference_count));
469525 }
470526 counts.sort();
471 assert_eq!(counts, [(0x120001, 26), (0x12004d, 12), (0x12004d, 25)]);
527 metadata::check(&raw, &raw, "Same author");
528 let author = metadata::author(&raw.objects[&raw.roots[&4]]);
529 assert_eq!(raw.objects[&author].jcid, 0x120001);
530 assert_eq!(counts, [(0x120001, 27), (0x12004d, 12), (0x12004d, 25)]);
472531 let document = Document::parse(&index).unwrap();
473532 let space = &document.spaces[&sid];
474533 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
crates/onestore/tests/page_movement.rs+56-14
......@@ -11,6 +11,8 @@ use onestore::{
1111mod current;
1212#[path = "support/disk.rs"]
1313mod disk;
14#[path = "support/metadata.rs"]
15mod metadata;
1416#[path = "support/sweep.rs"]
1517mod sweep;
1618
......@@ -91,14 +93,33 @@ fn verify(before: &[u8], after: &[u8], native: &[u8]) {
9193 let new = indexes[1]
9294 .resolve(*sid, indexes[1].spaces[sid].labels[&(ExGuid::default(), 1)])
9395 .unwrap();
94 assert_eq!(old.roots, new.roots);
95 assert_eq!(
96 old.objects.keys().collect::<Vec<_>>(),
97 new.objects.keys().collect::<Vec<_>>()
98 );
96 let changed = old.objects[&old.roots[&2]].data != new.objects[&new.roots[&2]].data;
97 let mut authors = std::collections::BTreeSet::new();
98 if changed {
99 metadata::check(&old, &new, "Author");
100 authors.insert(metadata::author(&new.objects[&new.roots[&4]]));
101 if let Some(id) = old.roots.get(&4) {
102 authors.extend(old.objects[id].references().unwrap().objects);
103 }
104 assert!(new.objects.keys().all(|id| {
105 old.objects.contains_key(id) || *id == new.roots[&4] || authors.contains(id)
106 }));
107 } else {
108 assert_eq!(old.roots, new.roots);
109 assert_eq!(
110 old.objects.keys().collect::<Vec<_>>(),
111 new.objects.keys().collect::<Vec<_>>()
112 );
113 }
99114 for (id, object) in &old.objects {
100 if *id != old.roots[&2] {
101 assert_eq!(format!("{object:?}"), format!("{:?}", new.objects[id]));
115 if *id != old.roots[&2] && !(changed && Some(id) == old.roots.get(&4)) {
116 let current = &new.objects[id];
117 assert_eq!(object.data, current.data);
118 assert_eq!(object.jcid, current.jcid);
119 assert_eq!(object.global_ids, current.global_ids);
120 if !authors.contains(id) {
121 assert_eq!(object.reference_count, current.reference_count);
122 }
102123 }
103124 }
104125 }
......@@ -383,13 +404,34 @@ fn metadata_copy_order_does_not_change_page_identity() {
383404 let stores = [edited.as_bytes(), expected.as_slice()].map(|b| Store::parse(b).unwrap());
384405 let indexes = stores.each_ref().map(|s| RevisionIndex::parse(s).unwrap());
385406 let documents = indexes.each_ref().map(|i| Document::parse(i).unwrap());
386 for sid in documents[0].spaces.keys() {
387 let nodes = documents.each_ref().map(|d| {
388 let space = &d.spaces[sid];
389 serde_json::to_value(&space.revisions[&space.contexts[&ExGuid::default()]].nodes)
390 .unwrap()
391 });
392 assert_eq!(nodes[0], nodes[1], "{sid}");
407 for space_id in documents[0].spaces.keys() {
408 let views = documents
409 .each_ref()
410 .map(|d| d.spaces[space_id].active().unwrap());
411 let mut nodes = views.map(|view| view.nodes.clone());
412 if *space_id == sid {
413 let raw = indexes.each_ref().map(|index| {
414 index
415 .resolve(
416 *space_id,
417 index.spaces[space_id].labels[&(ExGuid::default(), 1)],
418 )
419 .unwrap()
420 });
421 metadata::check(&raw[1], &raw[0], "Author");
422 let author = metadata::author(&raw[0].objects[&raw[0].roots[&4]]);
423 if !nodes[1].contains_key(&author) {
424 nodes[0].remove(&author).unwrap();
425 }
426 for (nodes, view) in nodes.iter_mut().zip(views) {
427 nodes.remove(&view.roots[&4]).unwrap();
428 }
429 }
430 assert_eq!(
431 serde_json::to_value(&nodes[0]).unwrap(),
432 serde_json::to_value(&nodes[1]).unwrap(),
433 "{space_id}"
434 );
393435 }
394436}
395437
crates/onestore/tests/paragraph.rs+28
......@@ -12,6 +12,8 @@ mod checkpoint;
1212mod current;
1313#[path = "support/disk.rs"]
1414mod disk;
15#[path = "support/metadata.rs"]
16mod metadata;
1517#[path = "support/ops.rs"]
1618mod ops;
1719
......@@ -220,6 +222,22 @@ fn joins_match_native_graphs_tags_and_inherited_character_styles() {
220222 .unwrap();
221223 let current_rid = current_document.spaces[sid].contexts[&ExGuid::default()];
222224 let new_raw = current_index.resolve(*sid, current_rid).unwrap();
225 let modified = metadata::check(&old_raw, &new_raw, "Author");
226 let author = metadata::author(&new_raw.objects[&new_raw.roots[&4]]);
227 changed.extend(old_raw.roots.get(&4));
228 for id in &changed {
229 if current
230 .nodes
231 .get(id)
232 .is_some_and(|node| matches!(node.kind, Kind::Paragraph { .. }))
233 {
234 assert_eq!(metadata::author(&new_raw.objects[id]), author);
235 assert_eq!(
236 current.nodes[id].modified.map(u64::from),
237 Some(modified / 10_000_000 - 11_960_006_400)
238 );
239 }
240 }
223241 for (id, object) in old_raw.objects {
224242 if !changed.contains(&id) {
225243 assert_eq!(
......@@ -409,6 +427,16 @@ fn splits_partition_native_paragraphs_at_every_scalar_boundary() {
409427 let new_raw = current_index
410428 .resolve(*sid, current_space.contexts[&ExGuid::default()])
411429 .unwrap();
430 let modified = metadata::check(&old_raw, &new_raw, "Author");
431 let author = metadata::author(&new_raw.objects[&new_raw.roots[&4]]);
432 changed.extend(old_raw.roots.get(&4));
433 for id in [paragraph, new_paragraph] {
434 assert_eq!(metadata::author(&new_raw.objects[&id]), author);
435 assert_eq!(
436 after.nodes[&id].modified.map(u64::from),
437 Some(modified / 10_000_000 - 11_960_006_400)
438 );
439 }
412440 for (id, object) in old_raw.objects {
413441 if !changed.contains(&id) {
414442 assert_eq!(object.data, new_raw.objects[&id].data, "untouched {id}");
crates/onestore/tests/support/checkpoint.rs+5-1
......@@ -26,7 +26,11 @@ pub fn pending(source: &[u8], space: ExGuid, text: ExGuid) -> Vec<u8> {
2626 with: with.into(),
2727 },
2828 };
29 for at in 0..512 - depth(source) as u64 {
29 for at in 0..=512 {
30 let image = section.image();
31 if depth(&image) == 512 {
32 return image;
33 }
3034 let ops = vec![retouch(0..0, "x"), retouch(0..1, "")];
3135 let at = 134_000_000_000_000_000 + at * 10_000_000;
3236 section.apply("Author", &Edit { at, ops }).unwrap();
crates/onestore/tests/support/metadata.rs created+119
......@@ -0,0 +1,119 @@
1use onestore::{ExGuid, IdStream, Object, ObjectData, PropertySets, ResolvedRevision, Value};
2
3pub fn author(object: &Object<'_>) -> ExGuid {
4 let ObjectData::Properties(bytes) = object.data else {
5 panic!("AuthorMostRecent needs a property object")
6 };
7 let fields = PropertySets::parse(bytes).unwrap();
8 let Value::References {
9 stream: IdStream::Objects,
10 compact_ids,
11 } = fields.sets[0]
12 .iter()
13 .find(|field| field.id == 0x20001d79)
14 .unwrap()
15 .value
16 else {
17 panic!("AuthorMostRecent needs one object reference")
18 };
19 let compact = u32::from_le_bytes(compact_ids.try_into().unwrap());
20 ExGuid {
21 guid: object.global_ids[&(compact >> 8)],
22 n: compact & 255,
23 }
24}
25
26/// Checks the revision context's author/time while preserving every other metadata field.
27pub fn check(before: &ResolvedRevision<'_>, after: &ResolvedRevision<'_>, name: &str) -> u64 {
28 let roots = |revision: &ResolvedRevision<'_>| {
29 revision
30 .roots
31 .iter()
32 .filter(|(kind, _)| **kind != 4)
33 .map(|(kind, id)| (*kind, *id))
34 .collect::<Vec<_>>()
35 };
36 assert_eq!(roots(before), roots(after));
37 let id = after.roots[&4];
38 let metadata = &after.objects[&id];
39 assert_eq!(metadata.jcid, 0x20044);
40 let ObjectData::Properties(bytes) = metadata.data else {
41 panic!()
42 };
43 let fields = PropertySets::parse(bytes).unwrap();
44 let Value::Bytes(time) = fields.sets[0]
45 .iter()
46 .find(|field| field.id == 0x18001d77)
47 .unwrap()
48 .value
49 else {
50 panic!()
51 };
52 let filetime = u64::from_le_bytes(time.try_into().unwrap());
53 assert!(filetime >= 119_600_064_000_000_000);
54 let author_id = author(metadata);
55 let named = &after.objects[&author_id];
56 assert_eq!(named.jcid, 0x120001);
57 let ObjectData::Properties(bytes) = named.data else {
58 panic!()
59 };
60 let named_fields = PropertySets::parse(bytes).unwrap();
61 let Value::Bytes(stored) = named_fields.sets[0]
62 .iter()
63 .find(|field| field.id == 0x1c001d75)
64 .unwrap()
65 .value
66 else {
67 panic!()
68 };
69 let expected: Vec<_> = name
70 .encode_utf16()
71 .chain([0])
72 .flat_map(u16::to_le_bytes)
73 .collect();
74 assert_eq!(stored, expected);
75 if let Some(old) = before.objects.get(&author_id) {
76 assert_eq!(old.data, named.data);
77 }
78 if let Some(old_id) = before.roots.get(&4) {
79 assert_eq!(*old_id, id);
80 let old = &before.objects[old_id];
81 assert_eq!(old.jcid, metadata.jcid);
82 assert!(metadata.global_ids.iter().all(|(entry, guid)| {
83 old.global_ids.get(entry) == Some(guid) || *guid == author_id.guid
84 }));
85 let ObjectData::Properties(old_bytes) = old.data else {
86 panic!()
87 };
88 let mut old_fields = PropertySets::parse(old_bytes).unwrap();
89 let mut new = fields;
90 for fields in [&mut old_fields, &mut new] {
91 fields.sets[0].retain(|field| ![0x18001d77, 0x20001d79].contains(&field.id));
92 }
93 assert_eq!(old_fields.sets, new.sets);
94 for field in old_fields.sets.iter().flatten() {
95 if let Value::References { compact_ids, .. } = field.value {
96 let ids = compact_ids.chunks_exact(4);
97 assert!(ids.remainder().is_empty());
98 for compact in ids {
99 let entry = u32::from_le_bytes(compact.try_into().unwrap()) >> 8;
100 assert_eq!(
101 metadata.global_ids.get(&entry),
102 Some(&old.global_ids[&entry])
103 );
104 }
105 }
106 }
107 } else {
108 assert!(!before.objects.contains_key(&id));
109 assert_eq!(fields.sets.len(), 1);
110 assert_eq!(
111 fields.sets[0]
112 .iter()
113 .map(|field| field.id)
114 .collect::<Vec<_>>(),
115 [0x18001d77, 0x20001d79]
116 );
117 }
118 filetime
119}