1#[path = "support/ops.rs"]
2mod ops;
3
4use onestore::{
5 ExGuid, PageEdit, RevisionIndex, Store,
6 document::{Document, Kind},
7 op::{PageOp, SectionOp},
8};
9
10#[path = "support/current.rs"]
11mod current;
12#[path = "support/disk.rs"]
13mod disk;
14#[path = "support/metadata.rs"]
15mod metadata;
16#[path = "support/sweep.rs"]
17mod sweep;
18
19const SOURCE: &[u8] =
20 include_bytes!("../../../corpus/page-lifecycle/04-nested/notebook/Lifecycle.one");
21
22fn pages(source: &[u8]) -> Vec<ExGuid> {
23 let store = Store::parse(source).unwrap();
24 let index = RevisionIndex::parse(&store).unwrap();
25 Document::parse(&index)
26 .unwrap()
27 .pages()
28 .unwrap()
29 .into_iter()
30 .map(|(sid, _)| sid)
31 .collect()
32}
33
34fn export(name: &str, bytes: &[u8]) {
35 if let Some(output) = std::env::var_os("ONESTORE_PAGE_MOVEMENT_OUTPUT") {
36 assert!(
37 std::path::Path::new(&output).is_absolute(),
38 "Use an absolute fixture output directory"
39 );
40 let path = std::path::Path::new(&output).join(name);
41 std::fs::create_dir_all(&path).unwrap();
42 std::fs::write(path.join("Lifecycle.one"), bytes).unwrap();
43 }
44}
45
46fn verify(before: &[u8], after: &[u8], native: &[u8]) {
47 let stores = [before, after, native].map(|bytes| Store::parse(bytes).unwrap());
48 let indexes = stores
49 .each_ref()
50 .map(|store| RevisionIndex::parse(store).unwrap());
51 indexes[1].validate_current().unwrap();
52 let documents = indexes
53 .each_ref()
54 .map(|index| Document::parse(index).unwrap());
55 let expected = documents[2].pages().unwrap();
56 assert_eq!(documents[1].pages().unwrap(), expected);
57 for (sid, _) in &expected {
58 let levels = [&documents[1], &documents[2]].map(|document| {
59 let space = &document.spaces[sid];
60 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
61 let Kind::Metadata { level, .. } = view.nodes[&view.roots[&2]].kind else {
62 panic!()
63 };
64 level
65 });
66 assert_eq!(levels[0], levels[1]);
67 }
68 let groups = [&documents[1], &documents[2]].map(|document| {
69 let space = &document.spaces[&document.root];
70 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
71 view.nodes[&view.roots[&1]]
72 .children
73 .iter()
74 .map(|id| view.nodes[id].spaces.clone())
75 .collect::<Vec<_>>()
76 });
77 assert_eq!(groups[0], groups[1]);
78 assert_eq!(
79 stores[1].header.transaction_count,
80 stores[0].header.transaction_count + 1
81 );
82 for (sid, space) in &indexes[0].spaces {
83 for rid in space.revisions.keys() {
84 assert_eq!(
85 format!("{:?}", indexes[0].resolve(*sid, *rid).unwrap()),
86 format!("{:?}", indexes[1].resolve(*sid, *rid).unwrap())
87 );
88 }
89 if *sid != indexes[0].root {
90 let old = indexes[0]
91 .resolve(*sid, space.labels[&(ExGuid::default(), 1)])
92 .unwrap();
93 let new = indexes[1]
94 .resolve(*sid, indexes[1].spaces[sid].labels[&(ExGuid::default(), 1)])
95 .unwrap();
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 }
114 for (id, object) in &old.objects {
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 }
123 }
124 }
125 }
126 }
127 let mut previous = after.to_vec();
128 previous[96..100].copy_from_slice(&before[96..100]);
129 assert_eq!(current::current(&previous), current::current(before));
130}
131
132#[test]
133fn explicit_page_edits_match_native_selection_and_indentation() {
134 let ids = pages(SOURCE);
135 let level = |i, n| PageEdit::set_level(ids[i], n).unwrap();
136 let move_to =
137 |i, before: Option<usize>, n| PageEdit::move_to(ids[i], before.map(|i| ids[i]), n).unwrap();
138 let cases: [(&str, Vec<PageEdit>, &[u8]); 8] = [
139 (
140 "01-demoted-parent",
141 vec![level(3, 2)],
142 include_bytes!(
143 "../../../corpus/page-lifecycle/movement/01-demoted-parent/notebook/Lifecycle.one"
144 ),
145 ),
146 (
147 "02-promoted-parent",
148 vec![level(3, 1)],
149 include_bytes!(
150 "../../../corpus/page-lifecycle/movement/02-promoted-parent/notebook/Lifecycle.one"
151 ),
152 ),
153 (
154 "03-selected-group-move",
155 vec![
156 move_to(3, None, 1),
157 move_to(4, None, 2),
158 move_to(5, None, 3),
159 ],
160 include_bytes!(
161 "../../../corpus/page-lifecycle/movement/03-selected-group-move/notebook/Lifecycle.one"
162 ),
163 ),
164 (
165 "04-subpage-move",
166 vec![move_to(4, Some(6), 1)],
167 include_bytes!(
168 "../../../corpus/page-lifecycle/movement/04-subpage-move/notebook/Lifecycle.one"
169 ),
170 ),
171 (
172 "05-insert-before-subpage",
173 vec![move_to(4, Some(5), 3)],
174 include_bytes!(
175 "../../../corpus/page-lifecycle/movement/05-insert-before-subpage/notebook/Lifecycle.one"
176 ),
177 ),
178 (
179 "06-promoted-level-two",
180 vec![level(4, 2)],
181 include_bytes!(
182 "../../../corpus/page-lifecycle/movement/06-promoted-level-two/notebook/Lifecycle.one"
183 ),
184 ),
185 (
186 "07-promoted-root",
187 vec![level(4, 1)],
188 include_bytes!(
189 "../../../corpus/page-lifecycle/movement/07-promoted-root/notebook/Lifecycle.one"
190 ),
191 ),
192 (
193 "08-collapsed-group-move",
194 vec![move_to(4, Some(0), 1), move_to(5, Some(0), 3)],
195 include_bytes!(
196 "../../../corpus/page-lifecycle/movement/08-collapsed-group-move/notebook/Lifecycle.one"
197 ),
198 ),
199 ];
200 let mut source = SOURCE.to_vec();
201 for (name, edits, native) in cases {
202 let serialized = serde_json::to_vec(&edits).unwrap();
203 let retained: Vec<PageEdit> = serde_json::from_slice(&serialized).unwrap();
204 assert_eq!(edits, retained);
205 let prepared = ops::section_op(&source, SectionOp::Pages(retained.to_vec())).unwrap();
206 verify(&source, prepared.as_bytes(), native);
207 assert_eq!(
208 ops::section_op(prepared.as_bytes(), SectionOp::Pages(retained.to_vec()))
209 .unwrap()
210 .as_bytes(),
211 prepared.as_bytes()
212 );
213 export(name, prepared.as_bytes());
214 source = prepared.as_bytes().to_vec();
215 }
216 let edit = PageEdit::move_to(ids[3], None, 1).unwrap();
217 let single = ops::section_op(SOURCE, SectionOp::Pages([edit].to_vec())).unwrap();
218 verify(
219 SOURCE,
220 single.as_bytes(),
221 include_bytes!(
222 "../../../corpus/page-lifecycle/movement/single-page/notebook/Lifecycle.one"
223 ),
224 );
225 export("single-page", single.as_bytes());
226}
227
228#[test]
229fn invalid_and_empty_page_edits_do_not_publish() {
230 let ids = pages(SOURCE);
231 for level in [0, 4, u32::MAX] {
232 assert!(PageEdit::set_level(ids[0], level).is_err());
233 }
234 assert!(PageEdit::move_to(ids[0], Some(ids[0]), 1).is_err());
235 assert!(PageEdit::set_level(ExGuid::default(), 1).is_err());
236 assert_eq!(
237 ops::section_op(SOURCE, SectionOp::Pages([].to_vec()))
238 .unwrap()
239 .as_bytes(),
240 SOURCE
241 );
242 assert_eq!(
243 ops::section_op(
244 SOURCE,
245 SectionOp::Pages([PageEdit::set_level(ids[0], 1).unwrap()].to_vec())
246 )
247 .unwrap()
248 .as_bytes(),
249 SOURCE
250 );
251 assert!(
252 ops::section_op(
253 SOURCE,
254 SectionOp::Pages([PageEdit::set_level(ids[0], 2).unwrap()].to_vec())
255 )
256 .is_err()
257 );
258 let duplicate = PageEdit::set_level(ids[3], 2).unwrap();
259 assert!(
260 ops::section_op(
261 SOURCE,
262 SectionOp::Pages([duplicate.clone(), duplicate].to_vec())
263 )
264 .is_err()
265 );
266 let missing = ExGuid {
267 guid: [0x77; 16],
268 n: 1,
269 };
270 assert!(
271 ops::section_op(
272 SOURCE,
273 SectionOp::Pages([PageEdit::set_level(missing, 1).unwrap()].to_vec())
274 )
275 .is_err()
276 );
277 assert!(
278 ops::section_op(
279 SOURCE,
280 SectionOp::Pages([PageEdit::move_to(ids[3], Some(missing), 1).unwrap()].to_vec())
281 )
282 .is_err()
283 );
284 let mut value = serde_json::to_value(PageEdit::set_level(ids[3], 2).unwrap()).unwrap();
285 value["level"] = 4.into();
286 let invalid: PageEdit = serde_json::from_value(value.clone()).unwrap();
287 assert!(ops::section_op(SOURCE, SectionOp::Pages([invalid].to_vec())).is_err());
288 value["unknown"] = true.into();
289 assert!(serde_json::from_value::<PageEdit>(value).is_err());
290}
291
292#[test]
293fn moving_pages_preserves_ink_file_data_and_native_feature_objects() {
294 for source in [
295 include_bytes!("../../../corpus/native-ink/20260905-ui/notebook/synthetic.one").as_slice(),
296 include_bytes!("../../../corpus/native-external-assets/notebook/synthetic.one").as_slice(),
297 include_bytes!("../../../corpus/m6/native-features-01/notebook/Features.one").as_slice(),
298 ] {
299 let first = pages(source)[0];
300 let created =
301 onestore::PageCreation::new(Some(first), Some("Movement anchor"), "Author").unwrap();
302 let source = ops::section_op(source, SectionOp::Create(created.clone()))
303 .unwrap()
304 .as_bytes()
305 .to_vec();
306 let moved = ops::section_op(
307 &source,
308 SectionOp::Pages([PageEdit::move_to(first, None, 1).unwrap()].to_vec()),
309 )
310 .unwrap();
311 assert_eq!(pages(moved.as_bytes()).last(), Some(&first));
312 let stores = [&source, moved.as_bytes()].map(|bytes| Store::parse(bytes).unwrap());
313 let indexes = stores
314 .each_ref()
315 .map(|store| RevisionIndex::parse(store).unwrap());
316 for (sid, space) in &indexes[0].spaces {
317 for rid in space.revisions.keys() {
318 assert_eq!(
319 format!("{:?}", indexes[0].resolve(*sid, *rid).unwrap()),
320 format!("{:?}", indexes[1].resolve(*sid, *rid).unwrap())
321 );
322 }
323 if *sid != indexes[0].root {
324 assert_eq!(space.labels, indexes[1].spaces[sid].labels);
325 let revision = indexes[0]
326 .resolve(*sid, space.labels[&(ExGuid::default(), 1)])
327 .unwrap();
328 for object in revision.objects.values() {
329 if let Some(onestore::FileDataReference::Internal(guid)) =
330 object.file_reference().unwrap()
331 {
332 assert_eq!(
333 stores[0].file_data(guid).unwrap(),
334 stores[1].file_data(guid).unwrap()
335 );
336 }
337 }
338 }
339 }
340 }
341}
342
343#[test]
344fn section_reordering_refreshes_stale_native_metadata_levels() {
345 let source = include_bytes!(
346 "../../../corpus/page-lifecycle/movement/02-promoted-parent/notebook/Lifecycle.one"
347 );
348 let ids = pages(source);
349 let edits: Vec<_> = ids[3..6]
350 .iter()
351 .enumerate()
352 .map(|(i, sid)| PageEdit::move_to(*sid, None, u32::try_from(i + 1).unwrap()).unwrap())
353 .collect();
354 let prepared = ops::section_op(source, SectionOp::Pages(edits.to_vec())).unwrap();
355 let stores = [source.as_slice(), prepared.as_bytes()].map(|b| Store::parse(b).unwrap());
356 let indexes = stores.each_ref().map(|s| RevisionIndex::parse(s).unwrap());
357 let documents = indexes.each_ref().map(|i| Document::parse(i).unwrap());
358 let levels = documents.each_ref().map(|document| {
359 let section = &document.spaces[&document.root];
360 let view = &section.revisions[&section.contexts[&ExGuid::default()]];
361 let copies: Vec<_> = view
362 .nodes
363 .iter()
364 .filter_map(|(id, node)| match &node.kind {
365 Kind::Metadata {
366 title: Some(title),
367 level,
368 } if title == "Same title" => Some((*id, *level)),
369 _ => None,
370 })
371 .collect();
372 assert_eq!(copies.len(), 1);
373 copies[0]
374 });
375 assert_eq!(levels[0].0, levels[1].0);
376 assert_eq!(levels[0].1, Some(2));
377 assert_eq!(levels[1].1, Some(1));
378 for (sid, space) in &indexes[0].spaces {
379 for rid in space.revisions.keys() {
380 assert_eq!(
381 format!("{:?}", indexes[0].resolve(*sid, *rid).unwrap()),
382 format!("{:?}", indexes[1].resolve(*sid, *rid).unwrap())
383 );
384 }
385 }
386}
387
388#[test]
389fn metadata_copy_order_does_not_change_page_identity() {
390 let source = include_bytes!(
391 "../../../corpus/page-lifecycle/page-edits/optional-cache/source-cold/notebook/Lifecycle.one"
392 );
393 let sid = pages(source)[3];
394 let edited = ops::section_op(
395 source,
396 SectionOp::Pages([PageEdit::set_level(sid, 2).unwrap()].to_vec()),
397 )
398 .unwrap();
399 let expected = include_bytes!(
400 "../../../corpus/page-lifecycle/page-edits/optional-cache/candidate/Lifecycle.one"
401 );
402 export("reordered-metadata", edited.as_bytes());
403 verify(source, edited.as_bytes(), expected);
404 let stores = [edited.as_bytes(), expected.as_slice()].map(|b| Store::parse(b).unwrap());
405 let indexes = stores.each_ref().map(|s| RevisionIndex::parse(s).unwrap());
406 let documents = indexes.each_ref().map(|i| Document::parse(i).unwrap());
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 );
435 }
436}
437
438#[test]
439fn native_edits_on_moved_pages_support_more_rust_changes() {
440 let source = include_bytes!(
441 "../../../corpus/page-lifecycle/page-edits/native/cold/notebook/Lifecycle.one"
442 );
443 let store = Store::parse(source).unwrap();
444 let index = RevisionIndex::parse(&store).unwrap();
445 let document = Document::parse(&index).unwrap();
446 let (sid, _) = *document.pages().unwrap().last().unwrap();
447 let space = &document.spaces[&sid];
448 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
449 assert!(
450 matches!(&view.nodes[&view.roots[&2]].kind, Kind::Metadata { title: Some(title), level: Some(1) } if title == "Native moved 🦋 é")
451 );
452 let bodies: Vec<_> = view.nodes.iter().filter_map(|(id, node)|
453 matches!(&node.kind, Kind::RichText { text, .. } if text == "Native body after a Rust page move.").then_some(*id)).collect();
454 let [body] = bodies.as_slice() else { panic!() };
455 let moved = ops::section_op(
456 source,
457 SectionOp::Pages([PageEdit::set_level(sid, 2).unwrap()].to_vec()),
458 )
459 .unwrap();
460 let edited = ops::page_op(
461 moved.as_bytes(),
462 sid,
463 PageOp::Text {
464 text: *body,
465 range: 0..0,
466 with: "Rust + ".into(),
467 },
468 )
469 .unwrap();
470 let after_store = Store::parse(edited.as_bytes()).unwrap();
471 let after_index = RevisionIndex::parse(&after_store).unwrap();
472 let after = Document::parse(&after_index).unwrap();
473 assert_eq!(after.pages().unwrap(), document.pages().unwrap());
474 let space = &after.spaces[&sid];
475 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
476 assert!(
477 matches!(&view.nodes[&view.roots[&2]].kind, Kind::Metadata { title: Some(title), level: Some(2) } if title == "Native moved 🦋 é")
478 );
479 assert!(
480 matches!(&view.nodes[body].kind, Kind::RichText { text, .. } if text == "Rust + Native body after a Rust page move.")
481 );
482 for (sid, space) in &index.spaces {
483 for rid in space.revisions.keys() {
484 assert_eq!(
485 format!("{:?}", index.resolve(*sid, *rid).unwrap()),
486 format!("{:?}", after_index.resolve(*sid, *rid).unwrap())
487 );
488 }
489 }
490 export("rust-followup", edited.as_bytes());
491}
492
493#[test]
494fn grouped_page_levels_and_membership_survive_each_storage_interruption() {
495 let mut source = onestore::create_section("movement.one", "Original", "Author").unwrap();
496 for _ in 0..2 {
497 let page = onestore::PageCreation::new(None, Some("Same title"), "Author").unwrap();
498 source = ops::section_op(&source, SectionOp::Create(page.clone()))
499 .unwrap()
500 .as_bytes()
501 .to_vec();
502 }
503 for (source, first) in [
504 (source.as_slice(), 1),
505 (
506 include_bytes!(
507 "../../../corpus/page-lifecycle/page-edits/optional-cache/source/Lifecycle.one"
508 )
509 .as_slice(),
510 3,
511 ),
512 ] {
513 let ids = pages(source);
514 let edits = [
515 PageEdit::set_level(ids[first], 2).unwrap(),
516 PageEdit::set_level(ids[first + 1], 3).unwrap(),
517 ];
518 let prepared = ops::section_op(source, SectionOp::Pages(edits.to_vec())).unwrap();
519 let old = current::current(source);
520 let new = current::current(prepared.as_bytes());
521 assert_eq!(new.len(), old.len());
522 // Byte-at-a-time writes tear the commit at every byte, a full sweep's matrix.
523 let limits: &[usize] = if sweep::full().is_some() {
524 &[1, 17, 4096]
525 } else {
526 &[17, 4096]
527 };
528 for &write_limit in limits {
529 let mut complete = disk::Disk {
530 visible: source.to_vec(),
531 durable: source.to_vec(),
532 operation: 0,
533 fail_at: None,
534 write_limit,
535 random: 1957,
536 };
537 prepared.commit(&mut complete).unwrap();
538 assert_eq!(complete.durable, prepared.as_bytes());
539 for fail_at in 1..=complete.operation {
540 let mut interrupted = disk::Disk {
541 visible: source.to_vec(),
542 durable: source.to_vec(),
543 operation: 0,
544 fail_at: Some(fail_at),
545 write_limit,
546 random: 1957 + fail_at as u64,
547 };
548 prepared.commit(&mut interrupted).unwrap_err();
549 let observed = current::current(&interrupted.durable);
550 assert!(
551 observed == old || observed == new,
552 "{write_limit}:{fail_at}"
553 );
554 }
555 }
556 }
557}
558
559#[test]
560fn repeated_nesting_retains_series_history_across_revision_checkpoints() {
561 let mut source = onestore::create_section("movement.one", "Original", "Author").unwrap();
562 let page = onestore::PageCreation::new(None, Some("Child"), "Author").unwrap();
563 source = ops::section_op(&source, SectionOp::Create(page.clone()))
564 .unwrap()
565 .as_bytes()
566 .to_vec();
567 let initial = source.clone();
568 for step in 0..520 {
569 let level = if step % 2 == 0 { 2 } else { 1 };
570 let edit = PageEdit::set_level(page.space(), level).unwrap();
571 let written = ops::section_op(&source, SectionOp::Pages([edit].to_vec()))
572 .unwrap()
573 .as_bytes()
574 .to_vec();
575 let store = Store::parse(&written).unwrap();
576 let index = RevisionIndex::parse(&store).unwrap();
577 index.validate_current().unwrap();
578 let document = Document::parse(&index).unwrap();
579 assert_eq!(document.pages().unwrap().len(), 2);
580 let space = &document.spaces[&page.space()];
581 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
582 assert!(
583 matches!(view.nodes[&view.roots[&2]].kind, Kind::Metadata { level: Some(n), .. } if n == level)
584 );
585 if step % 64 == 0 || matches!(step, 255 | 511 | 519) {
586 let mut old = written.clone();
587 old[96..100].copy_from_slice(&source[96..100]);
588 assert_eq!(current::current(&old), current::current(&source));
589 }
590 source = written;
591 }
592 let stores = [&initial, &source].map(|bytes| Store::parse(bytes).unwrap());
593 let indexes = stores
594 .each_ref()
595 .map(|store| RevisionIndex::parse(store).unwrap());
596 assert_eq!(
597 stores[1].header.transaction_count,
598 stores[0].header.transaction_count + 520
599 );
600 for (sid, space) in &indexes[0].spaces {
601 for rid in space.revisions.keys() {
602 assert_eq!(
603 format!("{:?}", indexes[0].resolve(*sid, *rid).unwrap()),
604 format!("{:?}", indexes[1].resolve(*sid, *rid).unwrap())
605 );
606 }
607 let current = &indexes[1].spaces[sid];
608 let rid = current.labels[&(ExGuid::default(), 1)];
609 assert!(
610 std::iter::successors(Some(rid), |rid| current.revisions[rid].dependency).count() < 20
611 );
612 }
613 if let Some(output) = std::env::var_os("ONESTORE_PAGE_MOVEMENT_STRESS_OUTPUT") {
614 assert!(
615 std::path::Path::new(&output).is_absolute(),
616 "Use an absolute fixture output directory"
617 );
618 std::fs::create_dir(&output).unwrap();
619 std::fs::write(std::path::Path::new(&output).join("movement.one"), &source).unwrap();
620 }
621}
622
623/// A move on a section OneNote merged, which holds empty page series, rewrites the series
624/// without them and gives the moved page one of its own.
625#[test]
626fn a_move_after_a_native_merge_drops_its_empty_series() {
627 let merged =
628 include_bytes!("../../../corpus/conflict-page/native-pages/merged/notebook/synthetic.one");
629 let series = |bytes: &[u8]| {
630 let store = Store::parse(bytes).unwrap();
631 let index = RevisionIndex::parse(&store).unwrap();
632 let document = Document::parse(&index).unwrap();
633 let space = &document.spaces[&document.root];
634 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
635 view.nodes[&view.roots[&1]]
636 .children
637 .iter()
638 .map(|id| (*id, view.nodes[id].spaces.clone()))
639 .collect::<Vec<_>>()
640 };
641 let before = series(merged);
642 assert!(before.iter().any(|(_, spaces)| spaces.is_empty()));
643 let ids = pages(merged);
644 let moved = ops::section_op(
645 merged,
646 SectionOp::Pages(vec![PageEdit::move_to(ids[1], None, 1).unwrap()]),
647 )
648 .unwrap();
649 let after = series(moved.as_bytes());
650 assert!(after.iter().all(|(_, spaces)| !spaces.is_empty()));
651 let (id, spaces) = after.last().unwrap();
652 assert_eq!(spaces, &vec![ids[1]]);
653 assert!(before.iter().all(|(old, _)| old != id));
654 assert_eq!(
655 pages(moved.as_bytes()),
656 [&ids[..1], &ids[2..], &ids[1..2]].concat()
657 );
658 if let Some(output) = std::env::var_os("ONESTORE_EMPTY_SERIES_EXPORT") {
659 let output = std::path::Path::new(&output);
660 std::fs::create_dir(output).unwrap();
661 std::fs::write(output.join("synthetic.one"), moved.as_bytes()).unwrap();
662 }
663}