haematite-cli 0.21.1

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
use serde_json::value::RawValue;
use serde_json::{Value, json};

use super::support::{Fixture, TestResult, request, values};

const SIX: std::num::NonZeroU64 = std::num::NonZeroU64::MIN.saturating_add(5);
const CHAIN: &str = "match (c:patients)-[:admitted_to]->(w:wards)<-[:staffed_by]-(n:nurses)";

fn expected(fixture: &Fixture) -> Result<Vec<Value>, Box<dyn std::error::Error>> {
    Ok(vec![
        json!({"c":fixture.value("patients", "p0")?,"w":{"ward":"A"},"n":{"name":"Nia"}}),
        json!({"c":fixture.value("patients", "p0")?,"w":{"ward":"A"},"n":{"name":"Noor"}}),
        json!({"c":fixture.value("patients", "p0")?,"w":{"ward":"B"},"n":{"name":"Nell"}}),
        json!({"c":{"name":"Bea"},"w":{"ward":"A"},"n":{"name":"Nia"}}),
        json!({"c":{"name":"Bea"},"w":{"ward":"A"},"n":{"name":"Noor"}}),
    ])
}

#[test]
fn match_two_hops_emit_exact_depth_first_rows_with_first_origin() -> TestResult {
    let fixture = Fixture::new()?;
    let answer = fixture.run(&request(CHAIN, SIX), &[])?;
    assert_eq!(values(&answer)?, expected(&fixture)?);
    let rows = answer
        .get("rows")
        .and_then(Value::as_array)
        .ok_or("rows missing")?;
    for (row, id) in rows.iter().zip(["p0", "p0", "p0", "p1", "p1"]) {
        assert_eq!(row["origin"]["id"], id);
        assert_eq!(row["origin"]["collection"], "patients");
        assert_eq!(row["origin"]["version"], fixture.revision("patients", id)?);
        assert!(row["origin"]["score"].is_null());
        assert_eq!(row["value"].as_object().ok_or("bindings missing")?.len(), 3);
    }
    assert_eq!(answer["records_read"], 18);
    assert!(answer["next"].is_null());
    Ok(())
}

#[test]
fn match_denied_middle_drops_its_entire_subtree_without_charging() -> TestResult {
    let mut fixture = Fixture::new()?;
    fixture.scan(6)?;
    let answer = fixture.run(&request(CHAIN, SIX), &[("wards", "w0")])?;
    assert_eq!(values(&answer)?, vec![expected(&fixture)?[2].clone()]);
    assert_eq!(answer["records_read"], 6);
    let all_denied = fixture.run(&request(CHAIN, SIX), &[("wards", "w0"), ("wards", "w1")])?;
    assert!(values(&all_denied)?.is_empty());
    assert_eq!(all_denied["records_read"], 2);
    Ok(())
}

#[test]
fn match_denied_last_record_drops_only_its_rows_without_charging() -> TestResult {
    let mut fixture = Fixture::new()?;
    fixture.scan(14)?;
    let answer = fixture.run(&request(CHAIN, SIX), &[("nurses", "n1")])?;
    let all = expected(&fixture)?;
    assert_eq!(
        values(&answer)?,
        vec![all[0].clone(), all[2].clone(), all[3].clone()]
    );
    assert_eq!(answer["records_read"], 14);
    fixture.scan(8)?;
    let empty = fixture.run(
        &request(CHAIN, SIX),
        &[("nurses", "n0"), ("nurses", "n1"), ("nurses", "n2")],
    )?;
    assert!(values(&empty)?.is_empty());
    assert_eq!(empty["records_read"], 8);
    Ok(())
}

#[test]
fn match_scan_allowance_charges_every_depth_and_refuses_an_incomplete_chain() -> TestResult {
    let mut fixture = Fixture::new()?;
    for limit in [1, 3, 4, 5, 6, 7, 8, 17] {
        fixture.scan(limit)?;
        let refusal = fixture
            .run(&request(CHAIN, SIX), &[])
            .err()
            .ok_or("partial chain accepted")?;
        assert_eq!(refusal.code, "query_scan_limit", "limit {limit}");
    }
    fixture.scan(18)?;
    let answer = fixture.run(&request(CHAIN, SIX), &[])?;
    assert_eq!(values(&answer)?, expected(&fixture)?);
    assert_eq!(answer["records_read"], 18);
    Ok(())
}

#[test]
fn match_repeated_binding_across_three_nodes_is_refused_by_name() -> TestResult {
    let fixture = Fixture::new()?;
    for query in [
        "match (c:patients)-[:admitted_to]->(w:wards)<-[:staffed_by]-(c:nurses)",
        "match (c:patients)-[:admitted_to]->(w:wards)<-[:staffed_by]-(w:nurses)",
    ] {
        let refusal = fixture
            .run(&request(query, SIX), &[])
            .err()
            .ok_or("repeated binding accepted")?;
        assert_eq!(refusal.code, "query_match_binding");
    }
    Ok(())
}

#[test]
fn match_second_hop_skips_far_labels_and_charges_only_the_edges() -> TestResult {
    let mut fixture = Fixture::new()?;
    fixture.scan(13)?;
    let answer = fixture.run(
        &request(
            "match (c:patients)-[:admitted_to]->(w:wards)<-[:staffed_by]-(n:patients)",
            SIX,
        ),
        &[],
    )?;
    assert!(values(&answer)?.is_empty());
    assert_eq!(answer["records_read"], 13);
    Ok(())
}

#[derive(serde::Deserialize)]
struct Answer {
    rows: Vec<Row>,
}

#[derive(serde::Deserialize)]
struct Row {
    value: Bindings,
}

#[derive(serde::Deserialize)]
struct Bindings {
    c: Box<RawValue>,
    w: Box<RawValue>,
    n: Box<RawValue>,
}

#[test]
fn match_chain_bindings_preserve_every_stored_record_byte() -> TestResult {
    let fixture = Fixture::new()?;
    let raw = fixture.run_raw(&request(CHAIN, SIX), &[])?;
    let answer: Answer = serde_json::from_str(raw.get())?;
    assert_eq!(answer.rows.len(), 5);
    for (row, patient, ward, nurse) in [
        (&answer.rows[0], "p0", "w0", "n0"),
        (&answer.rows[1], "p0", "w0", "n1"),
        (&answer.rows[2], "p0", "w1", "n2"),
        (&answer.rows[3], "p1", "w0", "n0"),
        (&answer.rows[4], "p1", "w0", "n1"),
    ] {
        assert_eq!(
            row.value.c.get().as_bytes(),
            fixture.bytes("patients", patient)?
        );
        assert_eq!(row.value.w.get().as_bytes(), fixture.bytes("wards", ward)?);
        assert_eq!(
            row.value.n.get().as_bytes(),
            fixture.bytes("nurses", nurse)?
        );
    }
    Ok(())
}