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::net::TcpListener;
use std::os::unix::fs::{PermissionsExt, symlink};
use std::os::unix::net::UnixListener;
use std::path::Path;
use std::process::{Command, Output};

const RECORD_FIRST: &str = "import sys\nwith open(sys.argv[1], 'a') as f: f.write('started\\n')\nprint('fixture ready', file=sys.stderr, flush=True)\nsys.stdin.read()";

fn check(fixture: &Fixture) -> Result<Output, Box<dyn std::error::Error>> {
    let path = fixture.root().join("door.json");
    fs::write(&path, serde_json::to_vec_pretty(&fixture.config)?)?;
    invoke(&path)
}

fn invoke(path: &Path) -> Result<Output, Box<dyn std::error::Error>> {
    Ok(Command::new(env!("CARGO_BIN_EXE_haematite"))
        .args(["door", "--check", "--config"])
        .arg(path)
        .current_dir(path.parent().ok_or("parent missing")?)
        .env_remove("OPENAI_API_KEY")
        .env_remove("ANTHROPIC_API_KEY")
        .env_remove("CLAUDE_CODE_OAUTH_TOKEN")
        .output()?)
}

fn text(output: &Output) -> String {
    format!(
        "{}{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    )
}

fn listing(path: &Path) -> std::io::Result<Vec<std::ffi::OsString>> {
    let mut entries = fs::read_dir(path)?
        .map(|entry| entry.map(|entry| entry.file_name()))
        .collect::<Result<Vec<_>, _>>()?;
    entries.sort();
    Ok(entries)
}

#[test]
fn r1_check_plans_upgrade_without_changing_bytes_mode_or_listing() -> TestResult {
    let mut fixture = Fixture::new()?;
    let mut first = fixture.child("first", RECORD_FIRST);
    let mut second = fixture.child("second", RECORD_FIRST);
    first
        .as_object_mut()
        .ok_or("child missing")?
        .remove("give_up_after");
    second
        .as_object_mut()
        .ok_or("child missing")?
        .remove("give_up_after");
    fixture.config["children"] = json!([first, second]);
    let path = fixture.root().join("door.json");
    let bytes = serde_json::to_vec_pretty(&fixture.config)?;
    fs::write(&path, &bytes)?;
    fs::set_permissions(&path, fs::Permissions::from_mode(0o640))?;
    let before = listing(fixture.root())?;
    let output = invoke(&path)?;
    assert_eq!(fs::read(&path)?, bytes);
    assert_eq!(fs::metadata(&path)?.permissions().mode() & 0o7777, 0o640);
    assert_eq!(listing(fixture.root())?, before);
    assert_eq!(output.status.code(), Some(0), "{}", text(&output));
    assert!(
        text(&output).contains("door_config_upgrade_needed: give_up_after=3 for first, second")
    );
    assert!(text(&output).contains("door_check_ok: 2 children, 1 stores"));
    Ok(())
}

#[test]
fn r1_check_asks_nothing_while_listener_and_store_are_held() -> TestResult {
    let mut fixture = Fixture::new()?;
    let listener = TcpListener::bind("127.0.0.1:0")?;
    let issuer = TcpListener::bind("127.0.0.1:0")?;
    let store = UnixListener::bind(fixture.root().join("store.sock"))?;
    listener.set_nonblocking(true)?;
    issuer.set_nonblocking(true)?;
    store.set_nonblocking(true)?;
    fixture.config["listen"] = json!(listener.local_addr()?.to_string());
    fixture.config["lys"]["issuer"] = json!(format!("http://{}/", issuer.local_addr()?));
    let assistant = fixture.root().join("assistant.sock");
    fixture.config["assistant"] = json!({"socket":assistant});
    fixture.config["children"] = json!([fixture.child("first", RECORD_FIRST)]);
    let output = check(&fixture)?;
    assert_eq!(output.status.code(), Some(0), "{}", text(&output));
    assert_eq!(
        issuer.accept().err().ok_or("issuer was asked")?.kind(),
        std::io::ErrorKind::WouldBlock
    );
    assert_eq!(
        listener.accept().err().ok_or("door was asked")?.kind(),
        std::io::ErrorKind::WouldBlock
    );
    assert_eq!(
        store.accept().err().ok_or("store was asked")?.kind(),
        std::io::ErrorKind::WouldBlock
    );
    assert!(!assistant.exists());
    assert!(!fixture.root().join("records").exists());
    Ok(())
}

fn program_refused(kind: &str) -> TestResult {
    let mut fixture = Fixture::new()?;
    let path = fixture.root().join("program");
    match kind {
        "directory" => fs::create_dir(&path)?,
        "nonexecutable" => fs::write(&path, b"fixture")?,
        "broken-symlink" => symlink(fixture.root().join("missing"), &path)?,
        "missing" => {}
        _ => return Err("unknown fixture kind".into()),
    }
    let healthy = fixture.child("healthy", RECORD_FIRST);
    let mut broken = fixture.child("broken", RECORD_FIRST);
    broken["program"] = json!(path);
    fixture.config["children"] = json!([healthy, broken]);
    let output = check(&fixture)?;
    assert!(!fixture.root().join("records").exists());
    assert_eq!(output.status.code(), Some(78), "{}", text(&output));
    assert!(
        text(&output).contains("door_child_program_missing: broken:"),
        "{}",
        text(&output)
    );
    Ok(())
}

#[test]
fn r1_check_missing_program_names_child_without_starting_any() -> TestResult {
    program_refused("missing")
}
#[test]
fn r1_check_directory_program_is_refused() -> TestResult {
    program_refused("directory")
}
#[test]
fn r1_check_nonexecutable_program_is_refused() -> TestResult {
    program_refused("nonexecutable")
}
#[test]
fn r1_check_broken_program_symlink_is_refused() -> TestResult {
    program_refused("broken-symlink")
}

#[test]
fn r1_check_resolves_executable_program_symlink() -> TestResult {
    let mut fixture = Fixture::new()?;
    let path = fixture.root().join("program");
    symlink("/usr/bin/python3", &path)?;
    let mut child = fixture.child("linked", RECORD_FIRST);
    child["program"] = json!(path);
    fixture.config["children"] = json!([child]);
    let output = check(&fixture)?;
    assert_eq!(output.status.code(), Some(0), "{}", text(&output));
    assert!(!fixture.root().join("records").exists());
    Ok(())
}

fn log_refused(readonly: bool) -> TestResult {
    let mut fixture = Fixture::new()?;
    let folder = fixture.root().join("logs");
    if readonly {
        fs::create_dir(&folder)?;
        fs::set_permissions(&folder, fs::Permissions::from_mode(0o500))?;
    }
    let mut child = fixture.child("broken-log", RECORD_FIRST);
    child["log"] = json!(folder.join("child.log"));
    fixture.config["children"] = json!([fixture.child("healthy", RECORD_FIRST), child]);
    let output = check(&fixture)?;
    if readonly {
        fs::set_permissions(&folder, fs::Permissions::from_mode(0o700))?;
    }
    assert!(!fixture.root().join("records").exists());
    assert_eq!(output.status.code(), Some(78), "{}", text(&output));
    assert!(
        text(&output).contains("door_child_log_unwritable: broken-log:"),
        "{}",
        text(&output)
    );
    Ok(())
}

#[test]
fn r1_check_unwritable_log_names_child_without_starting_any() -> TestResult {
    log_refused(true)
}
#[test]
fn r1_check_missing_log_folder_is_refused() -> TestResult {
    log_refused(false)
}

#[test]
fn r1_check_no_upgrade_preserves_bytes_and_reports_no_plan() -> TestResult {
    let fixture = Fixture::new()?;
    let path = fixture.root().join("door.json");
    let bytes = serde_json::to_vec_pretty(&fixture.config)?;
    fs::write(&path, &bytes)?;
    let before = listing(fixture.root())?;
    let output = invoke(&path)?;
    assert_eq!(output.status.code(), Some(0), "{}", text(&output));
    assert!(!text(&output).contains("door_config_upgrade_needed"));
    assert!(text(&output).contains("door_check_ok: 0 children, 1 stores"));
    assert_eq!(fs::read(&path)?, bytes);
    assert_eq!(listing(fixture.root())?, before);
    Ok(())
}

fn invalid_config(bytes: &[u8], refusal: &str) -> TestResult {
    let fixture = Fixture::new()?;
    let path = fixture.root().join("door.json");
    fs::write(&path, bytes)?;
    let output = invoke(&path)?;
    assert_eq!(output.status.code(), Some(78), "{}", text(&output));
    assert!(text(&output).contains(refusal), "{}", text(&output));
    assert_eq!(fs::read(&path)?, bytes);
    Ok(())
}

#[test]
fn r1_check_preserves_invalid_json_loader_refusal() -> TestResult {
    invalid_config(b"{invalid", "the door configuration is not valid:")
}

#[test]
fn r1_check_preserves_upgrade_planning_refusal() -> TestResult {
    invalid_config(br#"{"children":[{}]}"#, "door_config_upgrade_failed:")
}

#[test]
fn r1_check_plan_refuses_readonly_configuration_folder() -> TestResult {
    let mut fixture = Fixture::new()?;
    let mut child = fixture.child("first", RECORD_FIRST);
    child
        .as_object_mut()
        .ok_or("child missing")?
        .remove("give_up_after");
    fixture.config["children"] = json!([child]);
    let folder = fixture.root().join("readonly");
    fs::create_dir(&folder)?;
    let path = folder.join("door.json");
    let bytes = serde_json::to_vec_pretty(&fixture.config)?;
    fs::write(&path, &bytes)?;
    fs::set_permissions(&folder, fs::Permissions::from_mode(0o500))?;
    let output = invoke(&path)?;
    fs::set_permissions(&folder, fs::Permissions::from_mode(0o700))?;
    assert_eq!(output.status.code(), Some(78), "{}", text(&output));
    assert!(text(&output).contains("door_config_upgrade_failed:"));
    assert_eq!(fs::read(&path)?, bytes);
    assert_eq!(
        listing(&folder)?,
        vec![std::ffi::OsString::from("door.json")]
    );
    Ok(())
}

#[test]
fn r1_check_config_symlink_has_start_paths_named_refusal() -> TestResult {
    let fixture = Fixture::new()?;
    let source = fixture.root().join("source.json");
    let link = fixture.root().join("door.json");
    let bytes = serde_json::to_vec(&fixture.config)?;
    fs::write(&source, &bytes)?;
    symlink(&source, &link)?;
    let output = invoke(&link)?;
    assert_eq!(output.status.code(), Some(78), "{}", text(&output));
    assert!(text(&output).contains("door_config_upgrade_failed: configuration is a symlink:"));
    assert_eq!(fs::read_link(&link)?, source);
    assert_eq!(fs::read(&source)?, bytes);
    Ok(())
}

#[test]
fn r1_check_uses_start_paths_configuration_byte_limit() -> TestResult {
    let fixture = Fixture::new()?;
    let mut bytes = serde_json::to_vec(&fixture.config)?;
    bytes.resize(1_048_577, b' ');
    invalid_config(&bytes, "the door configuration exceeds its byte limit")?;
    Ok(())
}

fn same_start_refusal(log_failure: bool) -> TestResult {
    let mut fixture = Fixture::new()?;
    let folder = fixture.root().join("readonly-log");
    let mut child = fixture.child("broken", RECORD_FIRST);
    let expected = if log_failure {
        fs::create_dir(&folder)?;
        fs::set_permissions(&folder, fs::Permissions::from_mode(0o500))?;
        child["log"] = json!(folder.join("child.log"));
        "door_child_log_unwritable: broken:"
    } else {
        child["program"] = json!(fixture.root().join("missing-program"));
        "door_child_program_missing: broken:"
    };
    fixture.config["children"] = json!([child]);
    let mut door = fixture.start()?;
    let line = door.until_any(&[
        expected,
        "door_child_start_failed:",
        "door_child_log_failed:",
    ])?;
    drop(door);
    let output = check(&fixture)?;
    if log_failure {
        fs::set_permissions(&folder, fs::Permissions::from_mode(0o700))?;
    }
    assert!(
        line.starts_with(expected),
        "start used a different refusal: {line}"
    );
    assert_eq!(output.status.code(), Some(78), "{}", text(&output));
    assert!(text(&output).contains(expected), "{}", text(&output));
    assert!(!fixture.root().join("records").exists());
    Ok(())
}

#[test]
fn r1_start_and_check_share_program_prerequisite_refusal() -> TestResult {
    same_start_refusal(false)
}

#[test]
fn r1_start_and_check_share_log_prerequisite_refusal() -> TestResult {
    same_start_refusal(true)
}