haematite-cli 0.21.3

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
use super::Bound;
use crate::serve::query::parser::{self, Span, Stage};

fn read(text: &str, start: usize) -> Result<parser::Spanned<Stage>, String> {
    super::read(text, start).map_err(|error| error.to_string())
}

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

#[test]
fn window_versions_parse_with_the_original_byte_span() -> TestResult {
    let parsed = read("window(1200; 1300) | count", 23)?;
    assert_eq!(parsed.span, Span { start: 23, end: 41 });
    let Stage::Window(window) = parsed.value else {
        return Err("window parsed as another stage".into());
    };
    assert_eq!(window.from, Bound::Version(1200));
    assert_eq!(window.to, Bound::Version(1300));
    Ok(())
}

#[test]
fn window_is_dispatched_through_the_shared_parser() -> TestResult {
    let plan = parser::pipeline("patients | window(1200; 1300) | count")
        .map_err(|error| error.to_string())?;
    assert_eq!(plan.stages.len(), 2);
    assert!(matches!(plan.stages[0].value, Stage::Window(_)));
    assert_eq!(plan.stages[0].span, Span { start: 11, end: 29 });
    Ok(())
}

#[test]
fn window_reversed_versions_are_refused_by_name() -> TestResult {
    let refused = super::read("window(1400; 1300)", 11)
        .err()
        .ok_or("reversed window accepted")?;
    assert_eq!(refused.code(), "query_window_reversed");
    assert_eq!(refused.span(), Span { start: 11, end: 29 });
    Ok(())
}

#[test]
fn window_instants_normalize_offsets_and_keep_nanoseconds() -> TestResult {
    let parsed = read(
        "window(\"1970-01-01T01:00:00.000000001+01:00\"; \"1970-01-01T00:00:01Z\")",
        0,
    )?;
    let Stage::Window(window) = parsed.value else {
        return Err("instant window parsed as another stage".into());
    };
    assert_eq!(window.from, Bound::Instant(1));
    assert_eq!(window.to, Bound::Instant(1_000_000_000));
    Ok(())
}

#[test]
fn window_reversed_instants_are_refused_by_name() -> TestResult {
    let refused = super::read(
        "window(\"2026-10-13T01:00:00Z\"; \"2026-10-12T01:00:00Z\")",
        0,
    )
    .err()
    .ok_or("reversed instants accepted")?;
    assert_eq!(refused.code(), "query_window_reversed");
    Ok(())
}

#[test]
fn window_now_is_only_an_upper_bound() -> TestResult {
    let parsed = read("window(1200; $now)", 0)?;
    let Stage::Window(window) = parsed.value else {
        return Err("now window parsed as another stage".into());
    };
    assert_eq!(window.to, Bound::Now);
    let refused = super::read("window($now; 1300)", 0)
        .err()
        .ok_or("now accepted as from")?;
    assert_eq!(refused.code(), "query_window_bound");
    Ok(())
}

#[test]
fn window_malformed_bounds_are_named_without_defaults() -> TestResult {
    for text in [
        "window(1; \"1970-01-01T00:00:01Z\")",
        "window(-1; 2)",
        "window(1.5; 2)",
        "window(18446744073709551616; 2)",
        "window(1, 2)",
    ] {
        let refused = super::read(text, 0)
            .err()
            .ok_or("invalid bounds accepted")?;
        assert_eq!(refused.code(), "query_window_bound", "{text}");
    }
    for instant in [
        "2026-02-30T00:00:00Z",
        "2026-01-01",
        "2026-01-01T24:00:00Z",
        "2026-01-01T00:00:60Z",
        "1969-12-31T23:59:59Z",
        "2026-01-01T00:00:00-00:00",
    ] {
        let text = format!("window(\"{instant}\"; $now)");
        let refused = super::read(&text, 0)
            .err()
            .ok_or("invalid instant accepted")?;
        assert_eq!(refused.code(), "query_window_instant", "{instant}");
    }
    Ok(())
}

#[test]
fn window_cannot_follow_a_jq_value_stage() -> TestResult {
    let refused = parser::pipeline("patients | '.' | window(1; 2)")
        .err()
        .ok_or("window accepted after jq")?;
    assert_eq!(refused.code(), "query_parse");
    assert!(refused.to_string().contains("before the jq stage"));
    Ok(())
}

#[test]
fn window_query_check_returns_the_named_refusal_and_original_span() -> TestResult {
    use crate::serve::query::check::{self, CheckRequest, WordsRequest};
    let text = "patients | window(1400; 1300)";
    let checked = check::check(&CheckRequest {
        query: text.to_owned(),
    });
    assert_eq!(
        checked
            .get("refusal")
            .and_then(|refusal| refusal.get("code")),
        Some(&serde_json::json!("query_window_reversed"))
    );
    assert_eq!(
        checked
            .get("refusal")
            .and_then(|refusal| refusal.get("span")),
        Some(&serde_json::json!({"start":11,"end":29}))
    );
    let words = check::words(&WordsRequest {})?;
    let word = words
        .as_array()
        .ok_or("words missing")?
        .iter()
        .find(|word| word.get("name") == Some(&serde_json::json!("window")))
        .ok_or("window word missing")?;
    assert_eq!(word.get("status"), Some(&serde_json::json!("served")));
    assert_eq!(word.get("role"), Some(&serde_json::json!("bound")));
    Ok(())
}