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::support::{Fixture, TestResult};
use serde_json::json;
use std::fs;
use std::io::Read;

#[test]
fn r1_children_start_after_the_predecessors_ready_record() -> TestResult {
    let mut fixture = Fixture::new()?;
    let gate = fixture.root().join("release");
    assert!(
        std::process::Command::new("/usr/bin/mkfifo")
            .arg(&gate)
            .status()?
            .success()
    );
    let first = fixture.child("first", "import sys, os\np=sys.argv[1]\nwith open(os.path.join(os.path.dirname(p), 'release')) as gate: gate.read()\nwith open(p, 'a') as f: f.write('first ready\\n')\nprint('fixture ready first', file=sys.stderr, flush=True)\nsys.stdin.read()");
    let mut second = fixture.child("second", "import sys, os\np=sys.argv[1]\nreleased=os.path.exists(os.path.join(os.path.dirname(p), 'released'))\nwith open(p, 'a') as f: f.write('second started released='+str(released)+'\\n')\nprint('fixture ready second', file=sys.stderr, flush=True)\nsys.stdin.read()");
    second["after"] = json!("first");
    fixture.config["children"] = json!([first, second]);
    let mut door = fixture.start()?;
    // The dependency diagnostic and the fixture's ready event distinguish
    // waiting from an early start without measuring elapsed time.
    door.until_any(&[
        "door_child_waiting: second after first",
        "door_child_ready: second",
    ])?;
    fs::write(fixture.root().join("released"), b"released")?;
    fs::write(gate, b"release")?;
    door.until("haematite door: listening on ")?;
    assert_eq!(
        fs::read_to_string(fixture.root().join("records"))?,
        "first ready\nsecond started released=True\n"
    );
    assert!(fs::read_to_string(fixture.root().join("first.log"))?.contains("fixture ready first"));
    assert!(
        fs::read_to_string(fixture.root().join("second.log"))?.contains("fixture ready second")
    );
    Ok(())
}

fn refused(field: &str, change: impl FnOnce(&mut serde_json::Value)) -> TestResult {
    let mut fixture = Fixture::new()?;
    let child = fixture.child("first", "import sys\nwith open(sys.argv[1], 'a') as f: f.write('started\\n')\nprint('fixture ready first', file=sys.stderr, flush=True)\nsys.stdin.read()");
    fixture.config["children"] = json!([child]);
    change(&mut fixture.config["children"]);
    let mut door = fixture.start()?;
    let expected = format!("door_child_{field}_invalid:");
    let observed = door.until_any(&[&expected, "haematite door: listening on "])?;
    let mut stdout = door.child.stdout.take().ok_or("stdout missing")?;
    drop(door);
    stdout.read_to_end(&mut Vec::new())?;
    assert!(
        !fixture.root().join("records").exists(),
        "a malformed {field} child ran: {observed}"
    );
    assert!(
        observed.starts_with(&expected),
        "expected named {field} refusal, observed {observed}"
    );
    Ok(())
}

#[test]
fn r1_duplicate_name_is_refused_before_start() -> TestResult {
    refused("name", |c| {
        let first = c[0].clone();
        if let Some(list) = c.as_array_mut() {
            list.push(first);
        }
    })
}
#[test]
fn r1_invalid_name_is_refused_before_start() -> TestResult {
    refused("name", |c| c[0]["name"] = json!("Bad_Name"))
}
#[test]
fn r1_unknown_after_is_refused_before_start() -> TestResult {
    refused("after", |c| c[0]["after"] = json!("absent"))
}
#[test]
fn r1_unknown_store_is_refused_before_start() -> TestResult {
    refused("store", |c| c[0]["store"] = json!("absent"))
}
#[test]
fn r1_shared_store_is_refused_before_start() -> TestResult {
    refused("store", |c| {
        c[0]["store"] = json!("fixture");
        let mut second = c[0].clone();
        second["name"] = json!("second");
        if let Some(list) = c.as_array_mut() {
            list.push(second);
        }
    })
}
#[test]
fn r1_relative_program_is_refused_before_start() -> TestResult {
    refused("program", |c| c[0]["program"] = json!("python3"))
}
#[test]
fn r1_relative_log_is_refused_before_start() -> TestResult {
    refused("log", |c| c[0]["log"] = json!("child.log"))
}
#[test]
fn r1_empty_ready_is_refused_before_start() -> TestResult {
    refused("ready", |c| c[0]["ready"] = json!(""))
}