1#[path = "support/checkpoint.rs"]
2mod checkpoint;
3#[path = "support/current.rs"]
4mod current;
5#[path = "support/disk.rs"]
6mod disk;
7#[path = "support/metadata.rs"]
8mod metadata;
9#[path = "support/ops.rs"]
10mod ops;
11#[path = "support/sweep.rs"]
12mod sweep;
13
14use onestore::{
15 ExGuid, RevisionIndex, Store, TextAttribute as A,
16 document::{Document, Kind},
17 op::{Edit, Op, PageOp},
18};
19use std::ops::Range;
20
21/// Where an insertion goes: a paragraph in a container before a child of it, or last; or a
22/// new outline at a point on the page.
23#[derive(Clone, Copy)]
24enum Place {
25 In(ExGuid, Option<ExGuid>),
26 Outline(f32, f32),
27}
28
29/// The ops inserting `text` at `place` with `formats` set over UTF-16 ranges of it, and the
30/// identities of the new paragraph (or outline) and text.
31fn insertion(
32 space: ExGuid,
33 place: Place,
34 text: &str,
35 formats: &[(Range<u32>, Vec<A>)],
36) -> (Vec<Op>, ExGuid, ExGuid) {
37 let paragraph = ops::paragraph(text);
38 let text = paragraph.text().unwrap().id;
39 let (op, object) = match place {
40 Place::In(container, before) => {
41 let id = paragraph.id;
42 let paragraphs = vec![paragraph];
43 (
44 PageOp::Insert {
45 container,
46 before,
47 paragraphs,
48 },
49 id,
50 )
51 }
52 Place::Outline(x, y) => {
53 let object = ops::outline(x, y, vec![paragraph]);
54 let id = object.id();
55 (
56 PageOp::Add {
57 object,
58 before: None,
59 },
60 id,
61 )
62 }
63 };
64 let mut result = vec![Op::Page { space, op }];
65 for (range, set) in formats {
66 let op = PageOp::Format {
67 text,
68 range: range.clone(),
69 set: set.clone(),
70 clear: Vec::new(),
71 };
72 result.push(Op::Page { space, op });
73 }
74 (result, object, text)
75}
76
77fn targets(source: &[u8]) -> (ExGuid, ExGuid, ExGuid, ExGuid, ExGuid) {
78 let store = Store::parse(source).unwrap();
79 let index = RevisionIndex::parse(&store).unwrap();
80 let document = Document::parse(&index).unwrap();
81 let (sid, page) = document.pages().unwrap()[0];
82 let space = &document.spaces[&sid];
83 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
84 let outline = view.nodes[&page]
85 .children
86 .iter()
87 .copied()
88 .find(|id| matches!(view.nodes[id].kind, Kind::Outline { .. }))
89 .unwrap();
90 let paragraph = view.nodes[&outline].children[0];
91 let text = view.nodes[&paragraph].content[0];
92 (sid, page, outline, paragraph, text)
93}
94
95#[test]
96fn atomic_formatted_insertions_match_a_character_model_and_preserve_history() {
97 use serde_json::json;
98 let source = onestore::create_section("rich.one", "Original", "Author").unwrap();
99 let (sid, page, outline, _, _) = targets(&source);
100 let text = "abπŸ¦€ e\u{301} 東京\rEnd\t!";
101 let (plain, _, plain_text) = insertion(sid, Place::In(outline, None), text, &[]);
102 let base = ops::edited(&source, plain).unwrap();
103 let base_store = Store::parse(&base).unwrap();
104 let base_index = RevisionIndex::parse(&base_store).unwrap();
105 let base_doc = Document::parse(&base_index).unwrap();
106 let base_space = &base_doc.spaces[&sid];
107 let base_view = &base_space.revisions[&base_space.contexts[&ExGuid::default()]];
108 let default =
109 serde_json::to_value(&base_view.text_runs(plain_text).unwrap()[0].format).unwrap();
110 let offsets: Vec<u32> = std::iter::once(0)
111 .chain(text.chars().scan(0, |n, c| {
112 *n += c.len_utf16() as u32;
113 Some(*n)
114 }))
115 .collect();
116 let old_store = Store::parse(&source).unwrap();
117 let old_index = RevisionIndex::parse(&old_store).unwrap();
118 for seed in sweep::seeds(1..49, 48) {
119 let mut random = seed;
120 let mut expected: Vec<_> = text.chars().map(|c| (c, default.clone())).collect();
121 let mut formats = Vec::new();
122 // Descending construction also exercises the order formats apply in.
123 for i in (0..expected.len()).rev() {
124 random ^= random << 13;
125 random ^= random >> 7;
126 random ^= random << 17;
127 let (attributes, values) = match random % 8 {
128 0 => continue,
129 1 => (
130 vec![A::Bold(true), A::Underline(true)],
131 json!({"bold":true,"underline":true}),
132 ),
133 2 => (
134 vec![A::Italic(true), A::Strike(true)],
135 json!({"italic":true,"strike":true}),
136 ),
137 3 => (
138 vec![A::Font("Arial".into()), A::FontSize(13.5)],
139 json!({"font":"Arial","font_size":13.5}),
140 ),
141 4 => (
142 vec![
143 A::Color(Some([12, 34, 56])),
144 A::Highlight(Some([255, 255, 0])),
145 ],
146 json!({"color":0x38220c,"highlight":0x00ffff}),
147 ),
148 5 => (
149 vec![A::Superscript(true)],
150 json!({"superscript":true,"subscript":false}),
151 ),
152 6 => (
153 vec![A::Subscript(true)],
154 json!({"subscript":true,"superscript":false}),
155 ),
156 _ => (
157 vec![A::Bold(false), A::Color(None), A::Highlight(None)],
158 json!({"bold":false,"color":0xff000000_u32,"highlight":0xff000000_u32}),
159 ),
160 };
161 formats.push((offsets[i]..offsets[i + 1], attributes));
162 for (key, value) in values.as_object().unwrap() {
163 expected[i].1[key] = value.clone();
164 }
165 }
166 let place = if seed % 2 == 0 {
167 Place::Outline(72.0, 144.0)
168 } else {
169 Place::In(outline, None)
170 };
171 let (ops, _, inserted) = insertion(sid, place, text, &formats);
172 let edit = Edit {
173 at: ops::now(),
174 ops,
175 };
176 let restored: Edit = serde_json::from_slice(&serde_json::to_vec(&edit).unwrap()).unwrap();
177 assert_eq!(restored, edit);
178 let edited = ops::edited(&source, restored.ops).unwrap();
179 let store = Store::parse(&edited).unwrap();
180 assert_eq!(
181 store.header.transaction_count,
182 old_store.header.transaction_count + 1
183 );
184 let index = RevisionIndex::parse(&store).unwrap();
185 index.validate_current().unwrap();
186 assert_eq!(
187 index.spaces[&sid].revisions.len(),
188 old_index.spaces[&sid].revisions.len() + 1
189 );
190 for (space, old) in &old_index.spaces {
191 for revision in old.revisions.keys() {
192 assert_eq!(
193 format!("{:?}", old_index.resolve(*space, *revision).unwrap()),
194 format!("{:?}", index.resolve(*space, *revision).unwrap())
195 );
196 }
197 }
198 let document = Document::parse(&index).unwrap();
199 let space = &document.spaces[&sid];
200 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
201 if let Place::Outline(..) = place {
202 assert!(view.nodes[&page].children.len() > 1);
203 }
204 let actual: Vec<_> = view
205 .text_runs(inserted)
206 .unwrap()
207 .into_iter()
208 .flat_map(|run| {
209 let format = serde_json::to_value(run.format).unwrap();
210 run.text.chars().map(move |c| (c, format.clone()))
211 })
212 .collect();
213 assert_eq!(actual, expected, "seed {seed}");
214 }
215}
216
217#[test]
218fn formatted_empty_insertions_preserve_the_insertion_style_for_later_typing() {
219 let source = onestore::create_section("empty-rich.one", "Original", "Author").unwrap();
220 let (sid, _, outline, _, _) = targets(&source);
221 for place in [Place::Outline(72.0, 144.0), Place::In(outline, None)] {
222 let style = [(0..0, vec![A::Italic(true), A::FontSize(18.0)])];
223 let (ops, _, text) = insertion(sid, place, "", &style);
224 let inserted = ops::edited(&source, ops).unwrap();
225 let typed = ops::page_edited(
226 &inserted,
227 sid,
228 vec![PageOp::Text {
229 text,
230 range: 0..0,
231 with: "Typed πŸ¦€".into(),
232 }],
233 )
234 .unwrap();
235 for (bytes, expected) in [(&inserted, ""), (&typed, "Typed πŸ¦€")] {
236 let store = Store::parse(bytes).unwrap();
237 let index = RevisionIndex::parse(&store).unwrap();
238 index.validate_current().unwrap();
239 let document = Document::parse(&index).unwrap();
240 let space = &document.spaces[&sid];
241 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
242 let runs = view.text_runs(text).unwrap();
243 assert_eq!(runs.len(), 1);
244 assert_eq!(runs[0].text, expected);
245 assert_eq!(runs[0].format.italic, Some(true));
246 assert_eq!(runs[0].format.font_size, Some(18.0));
247 }
248 }
249}
250
251#[test]
252#[ignore = "exports formatted insertion candidates for cold native validation"]
253fn export_native_formatted_insertions() {
254 use std::{fs, path::PathBuf};
255 let output = PathBuf::from(std::env::var_os("ONESTORE_INSERT_OUTPUT").unwrap());
256 assert!(output.is_absolute());
257 fs::create_dir(&output).unwrap();
258 let generated = onestore::create_section("rich.one", "Original", "Author").unwrap();
259 let unicode = include_bytes!(
260 "../../../corpus/native/20260905-05/snapshots/03-format-unicode/notebook/synthetic.one"
261 );
262 let table = include_bytes!(
263 "../../../corpus/native/20260905-05/snapshots/07-table/notebook/synthetic.one"
264 );
265 let text = "Bold πŸ¦€ italic e\u{301} color 東京\rEnd";
266 let mut manifest = Vec::new();
267 for (name, source, empty) in [
268 ("outline", generated.as_slice(), false),
269 ("native-paragraph", unicode.as_slice(), false),
270 ("native-cell", table.as_slice(), false),
271 ("empty", generated.as_slice(), true),
272 ("empty-typed", generated.as_slice(), true),
273 ] {
274 let (sid, _, outline, paragraph, _) = targets(source);
275 let place = match name {
276 "native-paragraph" => Place::In(outline, Some(paragraph)),
277 "native-cell" => {
278 let store = Store::parse(source).unwrap();
279 let index = RevisionIndex::parse(&store).unwrap();
280 let document = Document::parse(&index).unwrap();
281 let space = &document.spaces[&sid];
282 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
283 let cell = *view
284 .nodes
285 .iter()
286 .find(|(_, node)| matches!(node.kind, Kind::Cell { .. }))
287 .unwrap()
288 .0;
289 Place::In(cell, None)
290 }
291 _ => Place::Outline(72.0, 144.0),
292 };
293 let formats = if empty {
294 vec![(0..0, vec![A::Italic(true), A::FontSize(18.0)])]
295 } else {
296 [
297 ("Bold", vec![A::Bold(true), A::Underline(true)]),
298 ("πŸ¦€", vec![A::Font("Arial".into()), A::FontSize(13.5)]),
299 ("italic", vec![A::Italic(true), A::Strike(true)]),
300 ("e\u{301}", vec![A::Superscript(true)]),
301 (
302 "color",
303 vec![
304 A::Color(Some([12, 34, 56])),
305 A::Highlight(Some([255, 255, 0])),
306 ],
307 ),
308 ("東京", vec![A::Subscript(true)]),
309 (
310 "End",
311 vec![
312 A::Bold(false),
313 A::Italic(false),
314 A::Color(None),
315 A::Highlight(None),
316 ],
317 ),
318 ]
319 .into_iter()
320 .map(|(label, attributes)| {
321 let start = text[..text.find(label).unwrap()].encode_utf16().count() as u32;
322 (
323 start..start + label.encode_utf16().count() as u32,
324 attributes,
325 )
326 })
327 .collect()
328 };
329 let (mut ops, object, inserted) =
330 insertion(sid, place, if empty { "" } else { text }, &formats);
331 if name == "empty-typed" {
332 let op = PageOp::Text {
333 text: inserted,
334 range: 0..0,
335 with: "Typed café πŸ¦€".into(),
336 };
337 ops.push(Op::Page { space: sid, op });
338 }
339 let edit = Edit {
340 at: ops::now(),
341 ops,
342 };
343 let bytes = ops::edited(source, edit.ops.clone()).unwrap();
344 current::current(&bytes);
345 let notebook = output.join(name);
346 fs::create_dir(&notebook).unwrap();
347 fs::write(notebook.join("synthetic.one"), &bytes).unwrap();
348 manifest.push(serde_json::json!({
349 "name": name, "space": sid, "object": object, "text": inserted, "edit": edit
350 }));
351 }
352 fs::write(
353 output.join("manifest.json"),
354 serde_json::to_vec_pretty(&manifest).unwrap(),
355 )
356 .unwrap();
357}
358
359#[test]
360fn paragraph_and_outline_insertions_publish_metadata_and_references_together() {
361 let source = onestore::create_section("insertion.one", "Original", "Original author").unwrap();
362 let (sid, page, outline, paragraph, original_text) = targets(&source);
363 for (place, expected_parent, expected_x, expected_y) in [
364 (Place::In(outline, Some(paragraph)), outline, None, None),
365 (Place::Outline(144.0, 18.0), page, Some(144.0), Some(18.0)),
366 ] {
367 let (ops, object, text) = insertion(sid, place, "First πŸ¦€\rSecond", &[]);
368 let transaction = ops::transaction(&source, "New author", ops)
369 .unwrap()
370 .unwrap();
371 let mut written = source.clone();
372 transaction.apply(&mut written).unwrap();
373 let store = Store::parse(&written).unwrap();
374 assert_eq!(
375 store.header.transaction_count,
376 Store::parse(&source).unwrap().header.transaction_count + 1
377 );
378 let index = RevisionIndex::parse(&store).unwrap();
379 index.validate_current().unwrap();
380 let document = Document::parse(&index).unwrap();
381 let space = &document.spaces[&sid];
382 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
383 assert!(view.nodes[&expected_parent].children.contains(&object));
384 assert_eq!(view.nodes[&object].layout.x, expected_x);
385 assert_eq!(view.nodes[&object].layout.y, expected_y);
386 assert!(
387 matches!(&view.nodes[&original_text].kind, Kind::RichText { text, .. } if text == "Original")
388 );
389 assert!(
390 matches!(&view.nodes[&text].kind, Kind::RichText { text, .. } if text == "First πŸ¦€\rSecond")
391 );
392 assert!(
393 matches!(&view.nodes[&page].kind, Kind::Page { alternate_title, .. } if alternate_title.as_deref() == Some("First πŸ¦€"))
394 );
395 assert!(
396 matches!(&view.nodes[&view.roots[&2]].kind, Kind::Metadata { title, .. } if title.as_deref() == Some("First πŸ¦€"))
397 );
398 assert!(
399 view.nodes
400 .values()
401 .any(|node| matches!(&node.kind, Kind::Author { name } if name.as_deref() == Some("New author")))
402 );
403 let runs = view.text_runs(text).unwrap();
404 assert_eq!(runs.len(), 1);
405 assert_eq!(runs[0].format.font.as_deref(), Some("Calibri"));
406 assert_eq!(runs[0].format.font_size, Some(11.0));
407 let previous_store = Store::parse(&source).unwrap();
408 let previous = RevisionIndex::parse(&previous_store).unwrap();
409 for (old_sid, old_space) in &previous.spaces {
410 for rid in old_space.revisions.keys() {
411 assert_eq!(
412 format!("{:?}", previous.resolve(*old_sid, *rid).unwrap()),
413 format!("{:?}", index.resolve(*old_sid, *rid).unwrap())
414 );
415 }
416 }
417 }
418}
419
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
474#[test]
475fn repeated_insertions_and_formatting_share_immutable_objects() {
476 let mut source = onestore::create_section("shared.one", "Original", "Same author").unwrap();
477 let (sid, _, outline, _, _) = targets(&source);
478 let mut texts = Vec::new();
479 for _ in 0..12 {
480 let (ops, _, text) = insertion(sid, Place::In(outline, None), "Repeated paragraph", &[]);
481 let transaction = ops::transaction(&source, "Same author", ops)
482 .unwrap()
483 .unwrap();
484 transaction.apply(&mut source).unwrap();
485 let bold = PageOp::Format {
486 text,
487 range: 1..8,
488 set: vec![A::Bold(true), A::FontSize(20.0)],
489 clear: Vec::new(),
490 };
491 let transaction = ops::transaction(
492 &source,
493 "Same author",
494 vec![Op::Page {
495 space: sid,
496 op: bold,
497 }],
498 )
499 .unwrap()
500 .unwrap();
501 transaction.apply(&mut source).unwrap();
502 texts.push(text);
503 }
504 let store = Store::parse(&source).unwrap();
505 let index = RevisionIndex::parse(&store).unwrap();
506 index.validate_current().unwrap();
507 let raw = index
508 .resolve(sid, index.spaces[&sid].labels[&(ExGuid::default(), 1)])
509 .unwrap();
510 let mut unique = std::collections::BTreeSet::new();
511 let mut counts = Vec::new();
512 for id in raw.reachable().unwrap() {
513 let object = &raw.objects[&id];
514 if object.jcid & 0x100000 == 0 {
515 continue;
516 }
517 let onestore::ObjectData::Properties(bytes) = object.data else {
518 panic!()
519 };
520 assert!(
521 unique.insert((object.jcid, bytes.to_vec())),
522 "Duplicate immutable object"
523 );
524 counts.push((object.jcid, object.reference_count));
525 }
526 counts.sort();
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)]);
531 let document = Document::parse(&index).unwrap();
532 let space = &document.spaces[&sid];
533 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
534 for text in texts {
535 let runs = view.text_runs(text).unwrap();
536 assert_eq!(runs.len(), 3);
537 assert_eq!(runs[1].format.bold, Some(true));
538 assert_eq!(runs[1].format.font_size, Some(20.0));
539 }
540}
541
542#[test]
543fn serialized_insertions_rebase_with_the_same_objects_and_preserve_remote_edits() {
544 let source = onestore::create_section("rebase.one", "Original", "Author").unwrap();
545 let (sid, _, outline, paragraph, text) = targets(&source);
546 let formats = [(0..3, vec![A::Bold(true), A::Color(Some([12, 34, 56]))])];
547 let (ops, object, inserted) = insertion(
548 sid,
549 Place::In(outline, Some(paragraph)),
550 "Inserted",
551 &formats,
552 );
553 let encoded = serde_json::to_vec(&ops).unwrap();
554 let restored: Vec<Op> = serde_json::from_slice(&encoded).unwrap();
555 assert_eq!(ops, restored);
556 let original = ops::edited(&source, ops).unwrap();
557 let remote = ops::page_edited(
558 &source,
559 sid,
560 vec![PageOp::Text {
561 text,
562 range: 0..0,
563 with: "Remote ".into(),
564 }],
565 )
566 .unwrap();
567 let updated = ops::edited(&remote, restored.clone()).unwrap();
568 let store = Store::parse(&updated).unwrap();
569 let index = RevisionIndex::parse(&store).unwrap();
570 let document = Document::parse(&index).unwrap();
571 let space = &document.spaces[&sid];
572 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
573 assert_eq!(view.nodes[&outline].children, [object, paragraph]);
574 assert!(
575 matches!(&view.nodes[&text].kind, Kind::RichText { text, .. } if text == "Remote Original")
576 );
577 assert!(
578 matches!(&view.nodes[&inserted].kind, Kind::RichText { text, .. } if text == "Inserted")
579 );
580 let runs = view.text_runs(inserted).unwrap();
581 assert_eq!(runs[0].text, "Ins");
582 assert_eq!(runs[0].format.bold, Some(true));
583 assert_eq!(runs[0].format.color, Some(0x38220c));
584 assert_eq!(runs[1].text, "erted");
585 assert_ne!(runs[1].format.bold, Some(true));
586 assert!(ops::edited(&original, restored.clone()).is_err());
587 assert!(ops::edited(&updated, restored).is_err());
588}
589
590#[test]
591#[cfg(any(unix, windows))]
592fn filesystem_insertion_commits_once_and_rejects_stale_replay() {
593 use std::{fs, io::Write};
594 let source = onestore::create_section("file.one", "Original", "Author").unwrap();
595 let (sid, _, outline, _, _) = targets(&source);
596 let (ops, object, _) = insertion(sid, Place::In(outline, None), "File insertion", &[]);
597 let path = std::env::temp_dir().join(format!("onestore-insertion-{object}.one"));
598 let mut file = fs::OpenOptions::new()
599 .write(true)
600 .create_new(true)
601 .open(&path)
602 .unwrap();
603 file.write_all(&source).unwrap();
604 file.sync_all().unwrap();
605 drop(file);
606 let transaction = ops::transaction(&source, "Author", ops).unwrap().unwrap();
607 let result = transaction.commit_file(&path);
608 let written = onestore::read_file(&path).unwrap();
609 let repeated = transaction.commit_file(&path).unwrap_err();
610 fs::remove_file(path).unwrap();
611 result.unwrap();
612 let mut expected = source.clone();
613 transaction.apply(&mut expected).unwrap();
614 assert_eq!(written, expected);
615 assert_eq!(repeated.state, onestore::CommitState::NotCommitted);
616 assert_eq!(repeated.error.kind(), std::io::ErrorKind::ResourceBusy);
617}
618
619#[test]
620fn anchors_targets_identities_and_text_are_validated_before_publication() {
621 let source = onestore::create_section("invalid.one", "Original", "Author").unwrap();
622 let (sid, page, outline, _, text) = targets(&source);
623 let refused = |source: &[u8], place, content: &str| {
624 ops::edited(source, insertion(sid, place, content, &[]).0).is_err()
625 };
626 assert!(refused(&source, Place::Outline(f32::NAN, 0.0), "Text"));
627 assert!(refused(&source, Place::Outline(0.0, f32::INFINITY), "Text"));
628 for content in ["a\0b", "a\nb", "a\u{fffc}b"] {
629 assert!(refused(&source, Place::In(outline, None), content));
630 }
631 assert!(
632 ops::transaction(
633 &source,
634 "a\0b",
635 insertion(sid, Place::In(outline, None), "Text", &[]).0
636 )
637 .is_err()
638 );
639 for place in [
640 Place::In(outline, Some(text)),
641 Place::In(text, None),
642 Place::In(page, None),
643 ] {
644 assert!(refused(&source, place, "Text"));
645 }
646 let (ops, object, _) = insertion(sid, Place::In(outline, None), "Text", &[]);
647 let created = ops::edited(&source, ops.clone()).unwrap();
648 let before_created = Place::In(outline, Some(object));
649 assert!(refused(&source, before_created, "Anchored"));
650 assert!(!refused(&created, before_created, "Anchored"));
651 // A new object's identity may not already be on the page.
652 assert!(ops::edited(&created, ops).is_err());
653}
654
655#[test]
656fn insertions_into_nested_paragraphs_and_native_table_cells_preserve_structure() {
657 let source = onestore::create_section("nested.one", "Original", "Author").unwrap();
658 let (sid, _, outline, paragraph, _) = targets(&source);
659 let mut nested = ops::paragraph("Nested");
660 nested.level = 2;
661 let child = nested.id;
662 let insert = PageOp::Insert {
663 container: paragraph,
664 before: None,
665 paragraphs: vec![nested],
666 };
667 let changed = ops::page_edited(&source, sid, vec![insert]).unwrap();
668 let store = Store::parse(&changed).unwrap();
669 let index = RevisionIndex::parse(&store).unwrap();
670 let document = Document::parse(&index).unwrap();
671 let space = &document.spaces[&sid];
672 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
673 assert_eq!(view.nodes[&outline].children, [paragraph]);
674 assert_eq!(view.nodes[&paragraph].children, [child]);
675 let source = include_bytes!(
676 "../../../corpus/native/20260905-05/snapshots/07-table/notebook/synthetic.one"
677 );
678 let store = Store::parse(source).unwrap();
679 let index = RevisionIndex::parse(&store).unwrap();
680 let document = Document::parse(&index).unwrap();
681 let (sid, cell) = document
682 .spaces
683 .iter()
684 .find_map(|(sid, space)| {
685 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
686 view.nodes.iter().find_map(|(id, node)| {
687 matches!(node.kind, Kind::Cell { .. }).then_some((*sid, *id))
688 })
689 })
690 .unwrap();
691 let (ops, _, _) = insertion(sid, Place::In(cell, None), "Added to cell", &[]);
692 current::current(&ops::edited(source, ops).unwrap());
693}
694
695#[test]
696fn insertion_publication_faults_expose_only_complete_graphs_and_title_caches() {
697 let original = onestore::create_section("atomic.one", "Original", "Author").unwrap();
698 let (sid, _, _, _, text) = targets(&original);
699 let checkpoint = checkpoint::pending(&original, sid, text);
700 for source in [
701 original.as_slice(),
702 include_bytes!("../../../corpus/append/round-01/tx-255/notebook/synthetic.one"),
703 &checkpoint,
704 ] {
705 let (sid, _, outline, paragraph, _) = targets(source);
706 for place in [
707 Place::In(outline, Some(paragraph)),
708 Place::Outline(0.0, 0.0),
709 ] {
710 let formats = [
711 (0..3, vec![A::Bold(true), A::FontSize(18.0)]),
712 (
713 6..8,
714 vec![A::Italic(true), A::Highlight(Some([255, 255, 0]))],
715 ),
716 ];
717 let (ops, _, _) = insertion(sid, place, "First πŸ¦€", &formats);
718 let edit = ops::transaction(source, "New author", ops)
719 .unwrap()
720 .unwrap();
721 let mut written = source.to_vec();
722 edit.apply(&mut written).unwrap();
723 let before = current::current(source);
724 let after = current::current(&written);
725 for write_limit in [17, 4096] {
726 let disk = |fail_at| disk::Disk {
727 visible: source.to_vec(),
728 durable: source.to_vec(),
729 operation: 0,
730 fail_at,
731 write_limit,
732 random: 945,
733 };
734 let mut successful = disk(None);
735 edit.commit(&mut successful).unwrap();
736 assert_eq!(successful.durable, written);
737 for at in 1..=successful.operation {
738 let mut interrupted = disk(Some(at));
739 let failure = edit.commit(&mut interrupted).unwrap_err();
740 let state = current::current(&interrupted.durable);
741 assert!(state == before || state == after, "interruption {at}");
742 if failure.state == onestore::CommitState::NotCommitted {
743 assert_eq!(state, before);
744 }
745 if failure.state == onestore::CommitState::Committed {
746 assert_eq!(state, after);
747 }
748 }
749 }
750 }
751 }
752}