liminal-cli 0.2.1

The liminal command: serve the network, maintain its stores, or serve its discovery MCP over stdio
Documentation
use std::error::Error;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};

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

fn command() -> Command {
    let mut command = Command::new(env!("CARGO_BIN_EXE_liminal"));
    for (name, _) in std::env::vars_os() {
        if name.to_string_lossy().starts_with("LIMINAL_") {
            command.env_remove(name);
        }
    }
    command
}

fn check(path: &Path) -> Result<Output, std::io::Error> {
    command().arg("check").arg("--config").arg(path).output()
}

fn toml_string(path: &Path) -> String {
    let escaped = path
        .display()
        .to_string()
        .replace('\\', "\\\\")
        .replace('"', "\\\"");
    format!("\"{escaped}\"")
}

fn fixture(root: &Path) -> Result<PathBuf, Box<dyn Error>> {
    let store = root.join("store");
    std::fs::create_dir(&store)?;
    let config = root.join("liminal.toml");
    std::fs::write(
        &config,
        format!(
            r#"listen_address = "127.0.0.1:7600"
health_listen_address = "127.0.0.1:7601"
drain_timeout_ms = 0
channels = []
routing_rules = []
persistence_path = {store}
[auth]
token = "test-token"
[auth.lys]
issuer = "https://unreachable.invalid"
network_id = "network-a"
keys_path = "absent-issuer.json"
live = true
[auth.lys.machine]
identity_path = "absent-identity.key"
pass_path = "absent-machine.pass"
"#,
            store = toml_string(&store),
        ),
    )?;
    Ok(config)
}

fn snapshot(root: &Path) -> Result<Vec<(PathBuf, Vec<u8>)>, std::io::Error> {
    let mut entries = vec![(root.to_path_buf(), Vec::new())];
    for entry in std::fs::read_dir(root)? {
        let path = entry?.path();
        if path.is_dir() {
            entries.extend(snapshot(&path)?);
        } else {
            entries.push((path.clone(), std::fs::read(path)?));
        }
    }
    entries.sort();
    Ok(entries)
}

fn assert_valid(output: &Output, path: &Path) {
    assert_eq!(
        output.status.code(),
        Some(0),
        "{}",
        String::from_utf8_lossy(&output.stderr)
    );
    assert_eq!(
        String::from_utf8_lossy(&output.stdout),
        format!("{}: configuration valid\n", path.display())
    );
    assert!(output.stderr.is_empty());
}

#[test]
fn check_accepts_valid_config_without_serving_or_writing() -> TestResult {
    let root = tempfile::tempdir()?;
    let path = fixture(root.path())?;
    let before = snapshot(root.path())?;
    let output = check(&path)?;
    assert_valid(&output, &path);
    assert_eq!(snapshot(root.path())?, before);
    Ok(())
}

#[test]
fn check_reports_each_startup_refusal_on_its_own_line() -> TestResult {
    let root = tempfile::tempdir()?;
    let path = fixture(root.path())?;
    let text = std::fs::read_to_string(&path)?
        .replace("127.0.0.1:7600", "127.0.0.1:0")
        .replace("127.0.0.1:7601", "127.0.0.1:0");
    let text = text
        .lines()
        .filter(|line| !line.starts_with("persistence_path"))
        .collect::<Vec<_>>()
        .join("\n");
    std::fs::write(&path, text)?;
    let output = check(&path)?;
    assert_eq!(output.status.code(), Some(2));
    assert!(output.stdout.is_empty());
    let stderr = String::from_utf8(output.stderr)?;
    let expected = concat!(
        "configuration validation failed: listen_address: port must be non-zero\n",
        "health_listen_address: port must be non-zero\n",
        "health_listen_address: must differ from listen_address for probe isolation\n",
        "persistence_path: full services require an existing directory; set persistence_path or LIMINAL_PERSISTENCE_PATH; no store is opened\n",
    );
    assert_eq!(stderr, expected);
    let startup = command().arg("--config").arg(&path).output()?;
    assert_eq!(startup.status.code(), Some(1));
    assert_eq!(
        String::from_utf8(startup.stderr)?,
        format!("Error: {expected}")
    );
    Ok(())
}

#[test]
fn check_applies_environment_overrides_before_validation() -> TestResult {
    let root = tempfile::tempdir()?;
    let path = fixture(root.path())?;
    let output = command()
        .arg("check")
        .arg("--config")
        .arg(&path)
        .env("LIMINAL_LISTEN_ADDRESS", "127.0.0.1:0")
        .output()?;
    assert_eq!(output.status.code(), Some(2));
    assert_eq!(
        String::from_utf8(output.stderr)?,
        "configuration validation failed: listen_address: port must be non-zero\n"
    );
    assert!(output.stdout.is_empty());
    Ok(())
}

#[test]
fn check_resolves_schemas_relative_to_the_configuration() -> TestResult {
    let root = tempfile::tempdir()?;
    let path = fixture(root.path())?;
    let text = std::fs::read_to_string(&path)?
        .replace("channels = []\n", "")
        .replace(
            "[auth]",
            "[[channels]]\nname = 'orders'\ndurable = true\nschema_ref = 'orders.json'\n[auth]",
        );
    std::fs::write(&path, text)?;
    std::fs::write(root.path().join("orders.json"), r#"{"type":"object"}"#)?;
    let elsewhere = tempfile::tempdir()?;
    let output = command()
        .arg("check")
        .arg("--config")
        .arg(&path)
        .current_dir(elsewhere.path())
        .output()?;
    assert_valid(&output, &path);
    std::fs::write(root.path().join("orders.json"), "not JSON")?;
    let output = check(&path)?;
    assert_eq!(output.status.code(), Some(2));
    assert!(String::from_utf8(output.stderr)?.contains("is not valid JSON"));
    Ok(())
}

#[test]
fn check_reports_missing_and_unreadable_files_with_the_os_error() -> TestResult {
    let root = tempfile::tempdir()?;
    for path in [root.path().join("missing.toml"), root.path().to_path_buf()] {
        let error = std::fs::read_to_string(&path)
            .err()
            .ok_or("file was readable")?;
        let output = check(&path)?;
        assert_eq!(output.status.code(), Some(2));
        assert!(output.stdout.is_empty());
        assert_eq!(
            String::from_utf8(output.stderr)?,
            format!(
                "configuration load failed: failed to read configuration file '{}': {error}\n",
                path.display()
            )
        );
    }
    Ok(())
}

#[test]
fn check_reports_malformed_toml_and_unknown_fields() -> TestResult {
    let root = tempfile::tempdir()?;
    let path = fixture(root.path())?;
    let valid = std::fs::read_to_string(&path)?;
    for text in [
        "[broken".to_owned(),
        format!("unknown_field = true\n{valid}"),
    ] {
        std::fs::write(&path, text)?;
        let error = liminal_server::config::load_from_file(&path)
            .err()
            .ok_or("malformed configuration loaded")?;
        let output = check(&path)?;
        assert_eq!(output.status.code(), Some(2));
        assert!(output.stdout.is_empty());
        let stderr = String::from_utf8(output.stderr)?;
        assert_eq!(stderr, format!("{error}\n"));
    }
    Ok(())
}

#[test]
fn check_requires_a_configuration_path() -> TestResult {
    let output = command().arg("check").output()?;
    assert_eq!(output.status.code(), Some(2));
    let stderr = String::from_utf8(output.stderr)?;
    assert!(stderr.contains("--config <CONFIG> is required"), "{stderr}");
    assert!(
        stderr.contains("liminal check --config <CONFIG>"),
        "{stderr}"
    );
    let output = check(Path::new(""))?;
    assert_eq!(output.status.code(), Some(2));
    assert_eq!(
        String::from_utf8(output.stderr)?,
        "configuration load failed: configuration path is empty\n"
    );
    Ok(())
}

#[test]
fn check_is_documented_in_both_help_pages() -> TestResult {
    for arguments in [vec!["--help"], vec!["check", "--help"]] {
        let output = command().args(arguments).output()?;
        assert_eq!(output.status.code(), Some(0));
        let stdout = String::from_utf8(output.stdout)?;
        assert!(
            stdout.contains("Validate the server configuration without serving"),
            "{stdout}"
        );
        assert!(stdout.contains("--config <CONFIG>"), "{stdout}");
    }
    Ok(())
}

#[test]
fn check_validates_extra_listen_addresses_without_serving_or_writing() -> TestResult {
    let root = tempfile::tempdir()?;
    let path = fixture(root.path())?;
    let original = std::fs::read_to_string(&path)?;
    std::fs::write(
        &path,
        format!("extra_listen_addresses = ['100.64.0.1:7600']\n{original}"),
    )?;
    let before = snapshot(root.path())?;
    assert_valid(&check(&path)?, &path);
    assert_eq!(snapshot(root.path())?, before);
    std::fs::write(
        &path,
        format!("extra_listen_addresses = ['0.0.0.0:7600']\n{original}"),
    )?;
    let before = snapshot(root.path())?;
    let output = check(&path)?;
    assert_eq!(output.status.code(), Some(2));
    assert!(output.stdout.is_empty());
    let error = String::from_utf8(output.stderr)?;
    assert!(
        error.contains("extra_listen_addresses[0] (0.0.0.0:7600)"),
        "{error}"
    );
    assert!(
        error.contains("unspecified addresses are refused"),
        "{error}"
    );
    assert_eq!(snapshot(root.path())?, before);
    Ok(())
}