minerva 0.2.0

Causal ordering for distributed systems
//! The identity-fork exhibit: the probe's third charge, produced on purpose.
//! The recipe's only "move" is delete-plus-reinsert (a fresh block dot at
//! the destination), and a fresh dot is a fresh identity: an edit concurrent
//! with the move lands on the tombstoned original and the moved copy never
//! sees it. This is stage C's motivating number, recorded here the way S181's
//! 8.5 ms fired arc 8: a stage that ships the move machine cites this test.

extern crate alloc;

use alloc::string::String;

use super::page::Page;
use super::render::Rendered;
use super::{PARAGRAPH, gossip_page};

#[test]
fn test_move_by_reinsert_forks_identity_and_loses_the_concurrent_edit() {
    let mut mover = Page::new(1);
    let mut editor = Page::new(2);

    // A paragraph block with "ab", fully replicated.
    let original = mover.create_block(PARAGRAPH, "draft", None);
    let char_a = mover.type_into(original, 'a', None);
    let char_b = mover.type_into(original, 'b', Some(char_a));
    gossip_page(&mover, &mut editor);
    assert_eq!(editor.para_text(original), "ab");

    // The mover "moves" the block the only way the recipe allows: tombstone
    // the original's order dot, mint a fresh block, copy the text as fresh
    // char dots, and retract the original's chars (the careful caller's
    // cleanup; the fork happens regardless).
    mover.delete_block_order_only(original);
    let reinserted = mover.create_block(PARAGRAPH, "draft", None);
    let copy_a = mover.type_into(reinserted, 'a', None);
    let _ = mover.type_into(reinserted, 'b', Some(copy_a));
    mover.retract_para_content(original);

    // Concurrently, the editor extends the block it still sees: 'c' after
    // 'b', an edit any move-aware sequence store would carry to the moved
    // block.
    let _ = editor.type_into(original, 'c', Some(char_b));
    assert_eq!(editor.para_text(original), "abc");

    // Converge every pair, both directions.
    gossip_page(&mover, &mut editor);
    gossip_page(&editor, &mut mover);
    gossip_page(&mover, &mut editor);

    assert_eq!(mover.render(), editor.render(), "the replicas agree");
    assert_eq!(
        mover.render(),
        [Rendered::Paragraph {
            block: reinserted,
            text: String::from("ab"),
        }],
        "the moved copy renders WITHOUT the concurrent edit: 'ab', never 'abc'"
    );

    // The lost edit survives in state, invisible in every render: it landed
    // on the tombstoned identity. One concurrent edit, one loss: the
    // identity-fork count this exhibit exists to record.
    assert!(
        !mover.block_order().contains(&original),
        "the original identity is order-tombstoned"
    );
    assert_eq!(
        mover.para_text(original),
        "c",
        "the edit lives under the dead identity ('a' and 'b' were retracted, \
         the concurrent 'c' survived the observed remove), unreachable by render"
    );
    let lost_edits = mover.para_visible_len(original);
    assert_eq!(lost_edits, 1, "the exhibit's number: one edit forked away");
}