haematite-cli 0.21.7

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
use std::io;

use serde_json::{Value, json, value::RawValue};

use super::{dispatch, writes_tests::Fixture};
use crate::mcp::{rpc, types};
use crate::serve::protocol;

type TestResult = Result<(), Box<dyn std::error::Error>>;

fn history(
    fixture: &mut Fixture,
    selection: &types::Selection,
    after: Option<&str>,
    record: Option<&types::RecordRef>,
    rows: u64,
) -> Result<types::ReadHistoryOutput, Box<dyn std::error::Error>> {
    let request = rpc::ToolCall {
        metadata: None,
        name: "read_history".to_owned(),
        arguments: serde_json::value::to_raw_value(
            &json!({"selection":selection,"after":after,"record":record,"page":{"rows":rows,"bytes":65536}}),
        )?,
    };
    let before = fixture.requests();
    let answer = dispatch(&mut fixture.sender, &request)?;
    assert_eq!(fixture.requests(), before + 2);
    assert!(!answer.is_error, "{}", answer.structured_content.get());
    let schema = crate::mcp::schema::tools()
        .into_iter()
        .find(|tool| tool.name == "read_history")
        .ok_or_else(|| io::Error::other("history schema missing"))?;
    let validator = jsonschema::validator_for(schema.output_schema.as_value())?;
    let body: Value = serde_json::from_str(answer.structured_content.get())?;
    validator
        .validate(&body)
        .map_err(|error| io::Error::other(error.to_string()))?;
    Ok(serde_json::from_str(answer.structured_content.get())?)
}

#[test]
fn read_history_preserves_served_snapshot_roots_and_raw_record() -> TestResult {
    let mut fixture = Fixture::start()?;
    fixture.seed()?;
    let raw = r#"{"n":1e2,"identity":18446744073709551616001}"#;
    fixture.mutate(
        "main",
        "put-large",
        protocol::Change::PutMany(protocol::DocumentImport {
            collection: "records".to_owned(),
            documents: vec![protocol::ImportedDocument {
                document_id: "large".to_owned(),
                document: RawValue::from_string(raw.to_owned())?,
            }],
        }),
    )?;
    let expected = fixture.current()?;
    let output = history(
        &mut fixture,
        &types::Selection {
            branch: "main".to_owned(),
            version: Some(types::Version(expected)),
        },
        None,
        Some(&types::RecordRef {
            collection: "records".to_owned(),
            document_id: "large".to_owned(),
        }),
        64,
    )?;
    assert_eq!(output.selection.version.0, expected);
    let record = output
        .record
        .ok_or_else(|| io::Error::other("record missing"))?;
    assert_eq!(record.document.get(), raw);
    assert_eq!(record.record.document_id, "large");
    assert!(!output.history.entries.is_empty());
    for entry in output.history.entries {
        assert_eq!(entry.branch, "main");
        assert_eq!(entry.roots.len(), 1);
        assert_eq!(entry.roots[0].shard, 0);
        assert!(!entry.roots[0].root.is_empty());
        let object = serde_json::to_value(entry)?;
        assert_eq!(object.as_object().map(serde_json::Map::len), Some(3));
        for absent in ["timestamp", "caller", "request_id", "change"] {
            assert!(object.get(absent).is_none());
        }
    }
    fixture.finish()
}

#[test]
fn read_history_continuation_stops_at_the_captured_version() -> TestResult {
    let mut fixture = Fixture::start()?;
    fixture.seed()?;
    let expected = fixture.current()?;
    let first = history(
        &mut fixture,
        &types::Selection {
            branch: "main".to_owned(),
            version: None,
        },
        None,
        None,
        1,
    )?;
    assert_eq!(first.selection.version.0, expected);
    let mut entries = first.history.entries;
    let mut after = first.history.next_after;
    fixture.mutate(
        "main",
        "later-collection",
        protocol::Change::Collection(protocol::CollectionWrite {
            collection: "later".to_owned(),
            schema: None,
        }),
    )?;
    let retained = history(
        &mut fixture,
        &types::Selection {
            branch: "main".to_owned(),
            version: Some(types::Version(expected)),
        },
        None,
        None,
        64,
    )?;
    assert_eq!(
        retained.history.entries.last().map(|entry| entry.version.0),
        Some(expected)
    );
    assert_eq!(
        u64::try_from(retained.history.entries.len())?,
        expected.sequence + 1
    );
    while let Some(cursor) = after {
        let answer = history(
            &mut fixture,
            &types::Selection {
                branch: "main".to_owned(),
                version: Some(types::Version(expected)),
            },
            Some(&cursor),
            None,
            1,
        )?;
        assert_eq!(answer.selection.version.0, expected);
        after = answer.history.next_after;
        entries.extend(answer.history.entries);
        assert!(u64::try_from(entries.len())? <= expected.sequence + 1);
    }
    assert_eq!(entries.last().map(|entry| entry.version.0), Some(expected));
    assert_eq!(u64::try_from(entries.len())?, expected.sequence + 1);
    fixture.finish()
}