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 std::cell::Cell;
use std::error::Error;
use std::fs;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::path::PathBuf;

use super::Config;
use serde_json::{Value, json};

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

thread_local! {
    static INPUT: Cell<Option<(PathBuf, u32)>> = const { Cell::new(None) };
}

pub fn input() -> Option<(PathBuf, u32)> {
    INPUT.with(|slot| {
        let input = slot.take();
        let copy = input.clone();
        slot.set(input);
        copy
    })
}

pub struct Fixture {
    pub(crate) directory: tempfile::TempDir,
    pub(crate) config_path: PathBuf,
    pub(crate) data_dir: PathBuf,
    pub(crate) value: Value,
}

impl Fixture {
    pub(crate) fn config(&self) -> Result<Config, serde_json::Error> {
        serde_json::from_value(self.value.clone())
    }
}

pub fn with_fixture(run: impl FnOnce(&Fixture) -> TestResult) -> TestResult {
    let directory = tempfile::Builder::new().prefix("i").tempdir_in("/tmp")?;
    fs::set_permissions(directory.path(), fs::Permissions::from_mode(0o700))?;
    let uid = fs::metadata(directory.path())?.uid();
    let data_dir = directory.path().join("database");
    fs::create_dir(&data_dir)?;
    let config_path = directory.path().join("service.json");
    let value = json!({
        "data_dir": data_dir,
        "branch_cache_bytes_per_shard": 1_048_576,
        "max_frame": 65_536, "max_clients": 1, "max_page": 8, "timeout_ms": 1000,
        "create": {"shards": 1, "cache_bytes_total": 1_048_576, "executor_threads": 1}
    });
    fs::write(&config_path, serde_json::to_vec(&value)?)?;
    let previous = INPUT.with(|slot| slot.replace(Some((directory.path().to_path_buf(), uid))));
    let fixture = Fixture {
        directory,
        config_path,
        data_dir,
        value,
    };
    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| run(&fixture)));
    INPUT.with(|slot| slot.set(previous));
    let cleanup = fixture.directory.close();
    match (result, cleanup) {
        (Ok(result), Ok(())) => result,
        (Ok(Ok(())), Err(error)) => Err(error.into()),
        (Ok(Err(test)), Err(cleanup)) => Err(format!("test: {test}; cleanup: {cleanup}").into()),
        (Err(panic), cleanup) => {
            if let Err(error) = cleanup {
                eprintln!("fixture cleanup: {error}");
            }
            std::panic::resume_unwind(panic)
        }
    }
}

#[test]
fn missing_socket_configuration_reads_without_runtime_effects() -> TestResult {
    with_fixture(|fixture| {
        fixture.config()?;
        assert!(!fixture.directory.path().join("haem").try_exists()?);
        Ok(())
    })
}

#[test]
fn startup_and_restart_choose_the_same_default_endpoint() -> TestResult {
    with_fixture(|fixture| {
        let config = fixture.config()?;
        let starting = config.endpoint(true)?;
        let restarting = config.endpoint(false)?;
        assert_eq!(starting.socket, restarting.socket);
        assert_eq!(starting.name, "database");
        assert_eq!(
            starting.socket.parent(),
            Some(fixture.directory.path().join("haem").as_path())
        );
        Ok(())
    })
}

#[test]
fn old_explicit_socket_and_logical_name_override_the_defaults() -> TestResult {
    with_fixture(|fixture| {
        let socket = fixture.directory.path().join("legacy.sock");
        let mut value = fixture.value.clone();
        value["socket"] = json!(socket);
        let config: Config = serde_json::from_value(value.clone())?;
        assert_eq!(config.socket.as_deref(), Some(socket.as_path()));
        assert!(config.name.is_none());
        assert_eq!(config.endpoint(false)?.socket, socket);
        assert!(!fixture.directory.path().join("haem").try_exists()?);
        value["name"] = json!("chosen");
        let named: Config = serde_json::from_value(value)?;
        assert_eq!(named.endpoint(false)?.name, "chosen");
        assert_eq!(named.endpoint(false)?.socket, socket);
        Ok(())
    })
}