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 crate::door::assistant_test_support::*;
use serde_json::{Value, json};
use std::{sync::atomic::Ordering, time::Duration};

const REASON: &str = "The dose is wrong for this child.";

fn invalid(f: &Fixture, body: &Value) -> Result {
    let text = written(f.answer("POST", "/decide", body))?;
    anyhow::ensure!(
        text.starts_with("HTTP/1.1 400 ") && text.contains("assistant_decision_invalid"),
        "invalid decision was admitted: {text}"
    );
    assert_eq!(f.door.issuer().draft_reads.load(Ordering::SeqCst), 0);
    assert_eq!(f.door.issuer().decisions.load(Ordering::SeqCst), 0);
    Ok(())
}

#[test]
fn r4_1_refusal_without_the_persons_reason_never_asks_lys() -> Result {
    let f = Fixture::new(json!({}))?;
    let received = ack(f.seat("agent-pass", "wards")?);
    invalid(&f, &json!({"draft":"draft-accepted","decision":"refuse"}))?;
    drop(received);
    Ok(())
}

#[test]
fn r4_1_refusal_reason_is_trimmed_and_reaches_the_issuer_exactly() -> Result {
    let f = Fixture::new(json!({}))?;
    let received = ack(f.seat("agent-pass", "wards")?);
    let text = written(f.answer(
        "POST",
        "/decide",
        &json!({"draft":"draft-accepted","decision":"refuse","reason":format!(" \n{REASON}\t ")}),
    ))?;
    anyhow::ensure!(text.starts_with("HTTP/1.1 200 "), "{text}");
    assert_eq!(
        f.door
            .issuer()
            .decision_requests
            .lock()
            .map_err(|_| anyhow::anyhow!("requests"))?
            .as_slice(),
        &[json!({"draft":"draft-accepted","decision":"refuse","reason":REASON})]
    );
    let told = received.recv()?;
    assert_eq!(told["op"]["decided"]["decision"], "refuse");
    // Fails while the door tells the seat the decision without the reason:
    // the seat records the person's words only when the op carries them.
    assert_eq!(told["op"]["decided"]["reason"], REASON);
    Ok(())
}

#[test]
fn r4_1_approval_refuses_any_reason_before_asking_lys() -> Result {
    let f = Fixture::new(json!({}))?;
    let seat = f.seat("agent-pass", "wards")?;
    for reason in [json!(REASON), json!(""), Value::Null, json!(3)] {
        invalid(
            &f,
            &json!({"draft":"draft-accepted","decision":"approve","reason":reason}),
        )?;
    }
    drop(seat);
    Ok(())
}

#[test]
fn r4_1_refusal_reason_has_one_to_five_hundred_characters() -> Result {
    let f = Fixture::new(json!({}))?;
    let seat = f.seat("agent-pass", "wards")?;
    for reason in [
        json!(" \t\n"),
        json!("é".repeat(501)),
        Value::Null,
        json!(3),
    ] {
        invalid(
            &f,
            &json!({"draft":"draft-accepted","decision":"refuse","reason":reason}),
        )?;
    }
    drop(seat);
    Ok(())
}

#[test]
fn r4_1_five_hundred_unicode_characters_are_accepted() -> Result {
    let f = Fixture::new(json!({}))?;
    let received = ack(f.seat("agent-pass", "wards")?);
    let reason = "é".repeat(500);
    check(
        f.answer(
            "POST",
            "/decide",
            &json!({"draft":"draft-accepted","decision":"refuse","reason":reason}),
        ),
        200,
        "refused",
    )?;
    assert_eq!(received.recv()?["op"]["decided"]["decision"], "refuse");
    Ok(())
}

#[tokio::test(flavor = "current_thread")]
async fn r4_1_native_lys_post_carries_the_persons_reason_exactly() -> Result {
    let listener = std::net::TcpListener::bind("127.0.0.1:0")?;
    let issuer = format!("http://{}", listener.local_addr()?);
    let worker = std::thread::spawn(move || -> Result {
        for step in 0..2 {
            let (socket, _) = listener.accept()?;
            let request =
                crate::door::http::read(&mut std::io::BufReader::new(&socket), 16384, 1_048_576)?;
            assert_eq!(request.header("authorization"), Some("Bearer person-pass"));
            let response = if step == 0 {
                crate::door::http::Response::json(
                    200,
                    &json!({"drafts":[{"id":"draft-native","app":"haematite","holder":"agent-assistant","responsible":PERSON,"target":{"kind":"haematite.database","id":"wards","action":"agent_permission"},"request_digest":"a".repeat(64)}],"next":null}),
                )
            } else {
                assert_eq!(request.path, "/api/product-drafts/draft-native/refuse");
                let body: Value = serde_json::from_slice(&request.body)?;
                assert_eq!(
                    body["reason"], REASON,
                    "Lys did not receive the person's own words"
                );
                crate::door::http::Response::json(
                    200,
                    &json!({"draft":"draft-native","state":"refused"}),
                )
            };
            response.write_to(&socket)?;
        }
        Ok(())
    });
    let client = crate::door::assistant_lys::Client {
        issuer,
        http: reqwest::Client::builder()
            .no_proxy()
            .timeout(Duration::from_secs(5))
            .build()?,
    };
    let answer = client
        .decide("person-pass", "draft-native", "refuse", Some(REASON))
        .await;
    worker.join().map_err(|_| anyhow::anyhow!("issuer"))??;
    assert_eq!(answer?["state"], "refused");
    Ok(())
}