haematite-cli 0.21.0

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
#![forbid(unsafe_code)]

use haematite_cli::serve::protocol::{Change, Operation, Request, Version};

#[test]
fn document_wire_preserves_payload_bytes_and_large_version_ids() -> Result<(), serde_json::Error> {
    let json = r#"{"id":"connection-1","operation":{"method":"mutation.apply","params":{
        "request_id":"durable-1","branch":"main",
        "expected":{"generation":"18446744073709551615","sequence":"1"},
        "change":{"kind":"document.put","value":{"collection":"events","document_id":"a",
        "document":{ "z": 1.00, "a": 18446744073709551616001 }}}
    }}}"#;
    let request: Request = serde_json::from_str(json)?;
    let Operation::Mutate(mutation) = request.operation else {
        return Err(serde::de::Error::custom("expected mutation"));
    };
    assert_eq!(mutation.expected.generation, u64::MAX);
    let Change::Put(document) = mutation.change else {
        return Err(serde::de::Error::custom("expected put"));
    };
    assert_eq!(
        document.document.get(),
        r#"{ "z": 1.00, "a": 18446744073709551616001 }"#
    );
    Ok(())
}

#[test]
fn versions_round_trip_without_browser_number_precision_loss() -> Result<(), serde_json::Error> {
    let version = Version {
        generation: u64::MAX,
        sequence: u64::MAX,
    };
    let encoded = serde_json::to_string(&version)?;
    assert_eq!(
        encoded,
        r#"{"generation":"18446744073709551615","sequence":"18446744073709551615"}"#
    );
    assert_eq!(serde_json::from_str::<Version>(&encoded)?, version);
    for invalid in [r#""01""#, r#""+1""#, "1", r#""18446744073709551616""#] {
        assert!(
            serde_json::from_str::<Version>(&format!(
                r#"{{"generation":{invalid},"sequence":"0"}}"#
            ))
            .is_err()
        );
    }
    Ok(())
}

#[test]
fn requests_refuse_unknown_fields_methods_and_zero_pages() {
    for invalid in [
        r#"{"id":"a","extra":true,"operation":{"method":"hello"}}"#,
        r#"{"id":"a","operation":{"method":"execute"}}"#,
        r#"{"id":"a","operation":{"method":"documents.list","params":{"selection":{"branch":"main"},"collection":"c","limit":0}}}"#,
        r#"{"id":"a","operation":{"method":"collections.list","params":{"branch":"main","anything":true}}}"#,
    ] {
        assert!(
            serde_json::from_str::<Request>(invalid).is_err(),
            "accepted {invalid}"
        );
    }
}

#[test]
fn collection_schema_removal_requires_explicit_null() -> Result<(), serde_json::Error> {
    use haematite_cli::serve::protocol::CollectionWrite;
    assert!(serde_json::from_str::<CollectionWrite>(r#"{"collection":"events"}"#).is_err());
    let removed: CollectionWrite =
        serde_json::from_str(r#"{"collection":"events","schema":null}"#)?;
    assert!(removed.schema.is_none());
    let refused: CollectionWrite =
        serde_json::from_str(r#"{"collection":"events","schema":false}"#)?;
    assert_eq!(refused.schema, Some(serde_json::Value::Bool(false)));
    Ok(())
}

#[test]
fn vector_batch_wire_is_declared_and_strict() -> Result<(), serde_json::Error> {
    assert!(Change::WIRE_NAMES.contains(&"vectors.put_many"));
    let value = r#"{"kind":"vectors.put_many","value":{"vectors":[{"collection":"notes","document_id":"a","field":"supplied","vector":[1.0,2.0]}]}}"#;
    let change: Change = serde_json::from_str(value)?;
    let Change::VectorPutMany(import) = change else {
        return Err(serde::de::Error::custom("expected vector batch"));
    };
    assert_eq!(import.vectors.len(), 1);
    assert_eq!(import.vectors[0].document_id, "a");
    for invalid in [
        r#"{"kind":"vectors.put_many","value":{"vectors":[],"extra":true}}"#,
        r#"{"kind":"vectors.put_many","value":{"vectors":[{"collection":"notes","document_id":"a","field":"supplied","vector":[1.0],"extra":true}]}}"#,
    ] {
        assert!(serde_json::from_str::<Change>(invalid).is_err());
    }
    Ok(())
}

#[test]
fn delete_batch_wire_is_declared_and_strict() -> Result<(), serde_json::Error> {
    assert!(Change::WIRE_NAMES.contains(&"documents.delete_many"));
    let value = r#"{"kind":"documents.delete_many","value":{"collection":"notes","document_ids":["b","a"]}}"#;
    let change: Change = serde_json::from_str(value)?;
    let Change::DeleteMany(delete) = change else {
        return Err(serde::de::Error::custom("expected delete batch"));
    };
    assert_eq!(delete.collection, "notes");
    assert_eq!(delete.document_ids, ["b", "a"]);
    assert!(
        serde_json::from_str::<Change>(
            r#"{"kind":"documents.delete_many","value":{"collection":"notes","document_ids":[],"extra":true}}"#,
        )
        .is_err()
    );
    Ok(())
}