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::frame;

use std::io::{self, Cursor};
use std::num::NonZeroU32;

#[test]
fn frames_round_trip_without_merging_adjacent_messages() -> io::Result<()> {
    let limit = NonZeroU32::new(16).ok_or_else(|| io::Error::other("invalid fixture limit"))?;
    let mut bytes = Vec::new();
    frame::write(&mut bytes, b"first", limit)?;
    frame::write(&mut bytes, b"second", limit)?;
    assert_eq!(bytes.get(..4), Some([0, 0, 0, 5].as_slice()));
    let mut input = Cursor::new(bytes);
    assert_eq!(frame::read(&mut input, limit)?, Some(b"first".to_vec()));
    assert_eq!(frame::read(&mut input, limit)?, Some(b"second".to_vec()));
    assert_eq!(frame::read(&mut input, limit)?, None);
    Ok(())
}

#[test]
fn cuts_are_errors_not_clean_disconnects() -> io::Result<()> {
    let limit = NonZeroU32::new(16).ok_or_else(|| io::Error::other("invalid fixture limit"))?;
    for bytes in [vec![0], vec![0, 0, 0], vec![0, 0, 0, 2, b'a']] {
        let Err(error) = frame::read(&mut Cursor::new(bytes), limit) else {
            return Err(io::Error::other("accepted a cut frame"));
        };
        assert_eq!(error.kind(), io::ErrorKind::UnexpectedEof);
    }
    Ok(())
}

#[test]
fn oversize_refusals_do_not_read_or_write_a_body() -> io::Result<()> {
    let limit = NonZeroU32::new(2).ok_or_else(|| io::Error::other("invalid fixture limit"))?;
    let mut input = Cursor::new(vec![0, 0, 0, 3, b'a', b'b', b'c']);
    assert!(frame::read(&mut input, limit).is_err());
    assert_eq!(input.position(), 4);
    let mut output = Vec::new();
    assert!(frame::write(&mut output, b"abc", limit).is_err());
    assert!(output.is_empty());
    assert!(frame::write(&mut output, b"", limit).is_err());
    Ok(())
}