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
use super::{handshake::Version, stream};
use serde_json::{Value, json};

#[test]
fn r3_rows_25_30_small_tool_input_survives_large_input_drops_whole() -> anyhow::Result<()> {
    let project = |input: Value| {
        stream::project(
            Version::One,
            json!({"sessionId":"s","update":{
        "sessionUpdate":"tool_call","toolCallId":"t","title":"Read","kind":"read","status":"completed","rawInput":input}}),
            "s",
            1024,
        )
    };
    let small =
        project(json!({"key":"small"}))?.ok_or_else(|| anyhow::anyhow!("small call absent"))?;
    assert_eq!(small["input"]["key"], "small");
    assert_eq!(small["truncated"], false);
    let large = project(json!({"key":"x".repeat(65536)}))?
        .ok_or_else(|| anyhow::anyhow!("large call absent"))?;
    assert!(large.get("input").is_none());
    assert_eq!(large["truncated"], true);
    assert!(serde_json::to_vec(&large)?.len() <= 1024);
    Ok(())
}

#[test]
fn r3_rows_25_26_28_29_projects_text_thought_plan_state() -> anyhow::Result<()> {
    for (update, kind) in [
        (
            json!({"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"hello"}}),
            "text",
        ),
        (
            json!({"sessionUpdate":"user_message_chunk","content":{"type":"text","text":"hello"}}),
            "text",
        ),
        (
            json!({"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"reason"}}),
            "thought",
        ),
        (
            json!({"sessionUpdate":"plan","entries":[{"content":"Read","status":"pending","priority":"high"}]}),
            "plan",
        ),
    ] {
        let item = stream::project(
            Version::One,
            json!({"sessionId":"s","update":update}),
            "s",
            1024,
        )?
        .ok_or_else(|| anyhow::anyhow!("item absent"))?;
        assert_eq!(item["kind"], kind);
    }
    let item = stream::project(Version::Two,json!({"sessionId":"s","update":{"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn"}}),"s",1024)?.ok_or_else(|| anyhow::anyhow!("state absent"))?;
    assert_eq!(item["kind"], "state");
    assert_eq!(item["stop_reason"], "end_turn");
    Ok(())
}

#[test]
fn r3_row_30_unicode_fits_including_envelope() -> anyhow::Result<()> {
    let mut item =
        json!({"kind":"text","text":"é🦀".repeat(1000),"conversation":"s","seq":1,"durable":false});
    stream::fit(&mut item, 256)?;
    assert!(serde_json::to_vec(&item)?.len() <= 256);
    assert_eq!(item["truncated"], true);
    assert!(
        item["text"]
            .as_str()
            .is_some_and(|s| s.is_char_boundary(s.len()))
    );
    Ok(())
}

#[test]
fn r3_rows_25_28_v2_whole_messages_and_plan_updates() -> anyhow::Result<()> {
    for (update, kind) in [
        (
            json!({"sessionUpdate":"agent_message","messageId":"m","content":[{"type":"text","text":"Whole message"}]}),
            "text",
        ),
        (
            json!({"sessionUpdate":"plan_update","plan":{"type":"items","planId":"p","entries":[{"content":"Read","status":"pending","priority":"high"}]}}),
            "plan",
        ),
    ] {
        let item = stream::project(
            Version::Two,
            json!({"sessionId":"s","update":update}),
            "s",
            1024,
        )?
        .ok_or_else(|| anyhow::anyhow!("whole update absent"))?;
        assert_eq!(item["kind"], kind);
        if kind == "text" {
            assert_eq!(item["text"], "Whole message");
            assert_eq!(item["message_id"], "m");
        } else {
            assert_eq!(item["entries"][0]["content"], "Read");
        }
    }
    Ok(())
}

#[test]
fn r3_row_25_metadata_message_does_not_clear_existing_content() -> anyhow::Result<()> {
    let project = |update| {
        stream::project(
            Version::Two,
            json!({"sessionId":"s","update":update}),
            "s",
            1024,
        )
    };
    let omitted = project(json!({"sessionUpdate":"agent_message","messageId":"m"}))?
        .ok_or_else(|| anyhow::anyhow!("metadata message absent"))?;
    assert!(
        omitted.get("text").is_none(),
        "omitted content became a replacement"
    );
    let cleared = project(json!({"sessionUpdate":"agent_message","messageId":"m","content":null}))?
        .ok_or_else(|| anyhow::anyhow!("clear message absent"))?;
    assert_eq!(cleared["text"], "");
    assert_eq!(cleared["replace"], true);
    Ok(())
}

#[test]
fn r1_stream_decode_failure_does_not_echo_harness_values() -> anyhow::Result<()> {
    for version in [Version::One, Version::Two] {
        let error = super::stream::project(
            version,
            json!({"sessionId":"session","update":"fixture-private-value"}),
            "session",
            65536,
        )
        .err()
        .ok_or_else(|| anyhow::anyhow!("malformed update accepted"))?;
        assert!(error.to_string().contains("acp_session_update_invalid"));
        assert!(!error.to_string().contains("fixture-private-value"));
    }
    Ok(())
}