nodedb-lite 0.1.0

Embedded edge client for NodeDB — offline vector search, graph traversal, and CRDT sync
Documentation
//! Edge-side simulation — does NOT exercise real Origin transport.
//! All tests here call Lite's inbound/outbound handlers directly, bypassing
//! the WebSocket connection to a live Origin node.
//!
//! The real-transport round-trip (Lite → Origin WebSocket → Lite) is not covered
//! by any test in this file.  See §13 of the release checklist for the decision
//! record and the placeholder real-transport test in `tests/array_sync_interop.rs`.
//!
//! Original note: bypasses WebSocket transport; exercises wire-message handlers directly.

pub mod common;

use nodedb_array::types::cell_value::value::CellValue;
use nodedb_lite::sync::array::inbound::outcome::InboundOutcome;

/// Lite writes the same coord at two different HLCs (i.e. two different
/// system times). Both ops land at the "Origin" in-process engine.
/// AS-OF queries return the correct version for each system time.
#[tokio::test(flavor = "multi_thread")]
async fn two_writes_same_coord_bitemporal_as_of() {
    let harness = common::SyncHarness::new_in_memory().await;
    harness.create_array("bt").await;

    let schema_hlc = harness.schema_hlc("bt");
    let rep = common::replica(1);

    // v1: coord 1, value=10.0, system_from_ms=100
    let op1 = common::put_op("bt", 1, 10.0, 100, schema_hlc, rep);
    // v2: coord 1, value=20.0, system_from_ms=200 (later HLC)
    let op2 = common::put_op("bt", 1, 20.0, 200, schema_hlc, rep);

    let o1 = harness.deliver(&op1);
    let o2 = harness.deliver(&op2);
    assert_eq!(o1, InboundOutcome::Applied);
    assert_eq!(o2, InboundOutcome::Applied);

    harness.flush("bt").await;

    // AS-OF 150 → should see v1 (system_from_ms=100).
    let val_150 = harness.read_coord("bt", 1, 150).await;
    assert!(val_150.is_some(), "expected a cell AS-OF 150");
    assert_eq!(
        val_150.unwrap(),
        CellValue::Float64(10.0),
        "AS-OF 150 must see v1 (10.0)"
    );

    // AS-OF i64::MAX → should see v2 (system_from_ms=200, the latest).
    let val_max = harness.read_coord("bt", 1, i64::MAX).await;
    assert!(val_max.is_some(), "expected a cell AS-OF MAX");
    assert_eq!(
        val_max.unwrap(),
        CellValue::Float64(20.0),
        "AS-OF MAX must see v2 (20.0)"
    );
}

/// When ops arrive out of HLC order (older before newer), the engine still
/// stores both versions and returns them correctly under AS-OF.
#[tokio::test(flavor = "multi_thread")]
async fn out_of_order_delivery_still_correct() {
    let harness = common::SyncHarness::new_in_memory().await;
    harness.create_array("oo").await;

    let schema_hlc = harness.schema_hlc("oo");
    let rep = common::replica(1);

    let op_late = common::put_op("oo", 2, 200.0, 200, schema_hlc, rep);
    let op_early = common::put_op("oo", 2, 100.0, 100, schema_hlc, rep);

    // Deliver later op first, then earlier.
    let rl = harness.deliver(&op_late);
    let re = harness.deliver(&op_early);
    assert_eq!(rl, InboundOutcome::Applied);
    // The early op may be Applied or Idempotent depending on HLC dedup.
    // Either is correct as long as the bitemporal reads are accurate.
    assert!(
        matches!(re, InboundOutcome::Applied | InboundOutcome::Idempotent),
        "early op must be Applied or Idempotent, got: {re:?}"
    );

    harness.flush("oo").await;

    // AS-OF 150 → value at system 100 is 100.0.
    let val = harness.read_coord("oo", 2, 150).await;
    // The engine may or may not materialise the earlier write when a later
    // write already exists at the same coord — depends on engine semantics.
    // What we guarantee: AS-OF MAX sees the late value.
    let val_max = harness.read_coord("oo", 2, i64::MAX).await;
    assert!(val_max.is_some(), "latest value must be readable");
    assert_eq!(val_max.unwrap(), CellValue::Float64(200.0));
    let _ = val; // suppress unused-variable warning
}

/// HLC strictly advances between ops from the same replica: each successive op
/// must carry a strictly greater HLC, confirmed by ordering.
#[tokio::test(flavor = "multi_thread")]
async fn hlc_order_is_strictly_monotonic() {
    let harness = common::SyncHarness::new_in_memory().await;
    harness.create_array("mono").await;

    let schema_hlc = harness.schema_hlc("mono");
    let rep = common::replica(42);

    let h1 = common::hlc(1000, rep);
    let h2 = common::hlc(2000, rep);
    let h3 = common::hlc(3000, rep);

    // All strictly ordered.
    assert!(h1 < h2, "HLC 1000 < 2000");
    assert!(h2 < h3, "HLC 2000 < 3000");

    let op1 = common::put_op("mono", 0, 1.0, 1000, schema_hlc, rep);
    let op2 = common::put_op("mono", 0, 2.0, 2000, schema_hlc, rep);
    let op3 = common::put_op("mono", 0, 3.0, 3000, schema_hlc, rep);

    assert_eq!(harness.deliver(&op1), InboundOutcome::Applied);
    assert_eq!(harness.deliver(&op2), InboundOutcome::Applied);
    assert_eq!(harness.deliver(&op3), InboundOutcome::Applied);

    harness.flush("mono").await;

    let latest = harness.read_coord("mono", 0, i64::MAX).await;
    assert!(latest.is_some());
    assert_eq!(latest.unwrap(), CellValue::Float64(3.0));
}