minerva 0.2.0

Causal ordering for distributed systems
//! Cross-pair coherence: the probe's first question. Four pairs mean four
//! contexts and no cross-pair atomicity, so gossip can deliver a block's
//! order ahead of its fields or its fields ahead of its content. These
//! scripts open every such window deliberately and test whether the
//! read-side conjunction rule (render a block only when every pair its
//! content needs has arrived; classify every other state) is sufficient:
//! candidate (a) of the stage-A scoping, the cheapest of the three.

extern crate alloc;

use alloc::string::String;

use super::page::{BlockDot, Page};
use super::render::Rendered;
use super::{PARAGRAPH, TABLE, gossip_fields, gossip_order, gossip_page, gossip_para};

/// Types a run into `block`, returning the last char's dot.
fn type_run(page: &mut Page, block: BlockDot, text: &str) -> Option<crate::metis::Dot> {
    let mut after = None;
    for ch in text.chars() {
        after = Some(page.type_into(block, ch, after));
    }
    after
}

#[test]
fn test_two_writers_converge_at_block_shape() {
    // The baseline: concurrent block creation on two replicas, full gossip,
    // one converged page. The recipes-not-types posture carries a block
    // editor's happy path with zero new surface.
    let mut a = Page::new(1);
    let mut b = Page::new(2);

    let para = a.create_block(PARAGRAPH, "intro", None);
    let _ = type_run(&mut a, para, "hi");
    let table = b.create_block(TABLE, "data", None);
    b.set_cell(table, 1, "x1");
    b.set_cell(table, 2, "x2");

    gossip_page(&a, &mut b);
    gossip_page(&b, &mut a);
    gossip_page(&a, &mut b); // second sweep: contexts learned in sweep one

    assert_eq!(a.block_order(), b.block_order());
    assert_eq!(a.render(), b.render());
    assert_eq!(a.anomalous_blocks(), 0, "a quiesced page renders whole");
    assert_eq!(a.para_text(para), "hi");
    assert_eq!(a.titles(para), ["intro"]);
    assert_eq!(a.table_rows(table), 2);
    assert_eq!(a.cell(table, 1), ["x1"]);
}

#[test]
fn test_order_ahead_of_content_is_a_classified_ghost() {
    // The order pair lands first: the receiver holds a visible block dot
    // with no fields and no content. A naive render fabricates a blank
    // block; the conjunction read classifies the window instead, and each
    // later pair's arrival narrows the classification until content renders.
    let mut writer = Page::new(1);
    let mut receiver = Page::new(2);

    let block = writer.create_block(PARAGRAPH, "intro", None);
    let _ = type_run(&mut writer, block, "hi");

    gossip_order(&writer, &mut receiver);
    assert_eq!(
        receiver.render(),
        [Rendered::Ghost { block }],
        "order without fields is a ghost, classified rather than fabricated"
    );
    assert_eq!(receiver.anomalous_blocks(), 1);

    gossip_fields(&writer, &mut receiver);
    assert_eq!(
        receiver.render(),
        [Rendered::KindPending { block }],
        "fields without the kind's content pair still withhold the render"
    );
    assert_eq!(receiver.anomalous_blocks(), 1);

    gossip_para(&writer, &mut receiver);
    assert_eq!(
        receiver.render(),
        [Rendered::Paragraph {
            block,
            text: String::from("hi"),
        }],
        "the conjunction closes when the last pair arrives"
    );
    assert_eq!(receiver.anomalous_blocks(), 0);
}

#[test]
fn test_content_ahead_of_order_is_silent() {
    // The reverse interleaving: fields and paragraph content land before the
    // order pair. The orphan content is held (the maps answer for it) but the
    // render is order-driven, so nothing user-visible fabricates; the window
    // costs nothing. This asymmetry (ghost classified, orphan silent) is the
    // recorded evidence that rule (a) suffices for *non-empty* creation
    // traffic (the empty-block ambiguity below is the recorded limit).
    let mut writer = Page::new(1);
    let mut receiver = Page::new(2);

    let block = writer.create_block(PARAGRAPH, "intro", None);
    let _ = type_run(&mut writer, block, "hi");

    gossip_fields(&writer, &mut receiver);
    gossip_para(&writer, &mut receiver);
    assert!(receiver.render().is_empty(), "no order, no render at all");
    assert_eq!(receiver.anomalous_blocks(), 0);
    assert!(
        receiver.fields_present(block) && receiver.para_present(block),
        "the orphan state is held and queryable, just never rendered"
    );

    gossip_order(&writer, &mut receiver);
    assert_eq!(
        receiver.render(),
        [Rendered::Paragraph {
            block,
            text: String::from("hi"),
        }]
    );
    assert_eq!(receiver.anomalous_blocks(), 0);
}

#[test]
fn test_empty_block_is_ambiguous_with_undelivered_content() {
    // The fourth recipe hurt, surfaced by review: canonical form drops
    // bottom stores, so a block whose content is genuinely empty has no key
    // in its kind's content map, and a *fully delivered* empty block
    // classifies exactly like one whose content pair has not arrived.
    // Absence cannot say "empty". The conjunction rule is therefore honest
    // for NON-EMPTY traffic only; an empty block parks as kind-pending
    // forever, on the writer's own render as much as the peer's. The
    // caller-side cure is an initialization write (a marker dot spent to
    // make presence carry "empty"); the structural cure is the stage-B
    // one-pair node, where the kind tag and its content ride one delta and
    // tag-present-with-bottom-content *means* empty.
    let mut writer = Page::new(1);
    let mut receiver = Page::new(2);

    let block = writer.create_block(PARAGRAPH, "empty", None);
    assert_eq!(
        writer.anomalous_blocks(),
        1,
        "even the writer's own conjunction read cannot say 'empty'"
    );

    gossip_page(&writer, &mut receiver);
    assert_eq!(
        receiver.render(),
        [Rendered::KindPending { block }],
        "a fully delivered empty block is indistinguishable from an undelivered one"
    );

    // The first content write disambiguates: presence begins at the first
    // dot, and the window closes only then.
    let _ = writer.type_into(block, 'h', None);
    gossip_para(&writer, &mut receiver);
    assert_eq!(
        receiver.render(),
        [Rendered::Paragraph {
            block,
            text: String::from("h"),
        }]
    );
    assert_eq!(receiver.anomalous_blocks(), 0);
}