liminal-cli 0.3.30

The liminal command: serve the network, maintain its stores, or serve its discovery MCP over stdio
Documentation
//! ACCESS-012 R3: an open exchange (no `[auth]`) with one top-level
//! `[[templates]]` runs the service lifecycle for a seat that declares a
//! name, through the liminal binary: `seat init --open --name`, then
//! `services`, `register`, `start`, `call`, `stop` and `retire`, every
//! record under `open:<name>`; a seat with no name acts as `open:anonymous`.

use std::error::Error;
use std::io::Read;
use std::os::unix::net::UnixListener;
use std::path::Path;
use std::process::Command;

use serde_json::json;

use super::network::OpenServer;
use super::{Host, Ticket};

/// The top-level `[[templates]]` of the `echo` service, whose process is
/// this test binary's `lifecycle::service_process`, told the ready socket.
fn template(ready: &Path) -> Result<String, Box<dyn Error>> {
    let exe = std::env::current_exe()?;
    let args = serde_json::to_string(&[
        "-c",
        "export LIMINAL_LIFECYCLE_READY=\"$1\"; shift; exec \"$@\"",
        "service-ready",
        ready.to_str().ok_or("ready path is not Unicode")?,
        exe.to_str().ok_or("executable path is not Unicode")?,
        "--exact",
        "lifecycle::service_process",
        "--nocapture",
    ])?;
    Ok(format!(
        "[[templates]]\nname = 'echo'\ncommand = '/bin/sh'\nargs = {args}\nsubjects = ['echo']\nstop_timeout_ms = 2000\n"
    ))
}

/// `liminal seat init --open <server> [--name <name>] --out <seat>`.
pub fn seat_init(server: &str, name: Option<&str>, out: &Path) -> Result<(), Box<dyn Error>> {
    let mut command = Command::new(env!("CARGO_BIN_EXE_liminal"));
    command
        .args(["seat", "init", "--open", server, "--out"])
        .arg(out);
    if let Some(name) = name {
        command.args(["--name", name]);
    }
    let output = command.output()?;
    if !output.status.success() {
        return Err(format!(
            "seat init exited {}: {}",
            output.status,
            String::from_utf8_lossy(&output.stderr)
        )
        .into());
    }
    Ok(())
}

#[test]
fn access012_an_open_exchange_runs_a_named_seats_services_through_the_binary()
-> Result<(), Box<dyn Error>> {
    let dir = tempfile::tempdir()?;
    let ready_path = dir.path().join("ready.sock");
    let ready = UnixListener::bind(&ready_path)?;
    let server = OpenServer::start_on_with(dir.path(), "127.0.0.1", &template(&ready_path)?)?;
    let address = server.address.to_string();

    let seat = dir.path().join("hermes.toml");
    seat_init(&address, Some("hermes"), &seat)?;
    let text = std::fs::read_to_string(&seat)?;
    assert!(text.contains("[open]\nname = \"hermes\""), "{text}");
    let mut host = Host::open(Ticket::spawn(&seat)?, true)?;

    let listed = host.tool("services", &json!({}))?;
    assert_eq!(listed["isError"], false, "{listed}");
    assert_eq!(
        listed["structuredContent"]["templates"][0]["name"], "echo",
        "{listed}"
    );
    assert_eq!(
        listed["structuredContent"]["services"],
        json!([]),
        "{listed}"
    );

    let made = host.tool("register", &json!({"template": "echo", "name": "one"}))?;
    assert_eq!(made["isError"], false, "{made}");
    let made = &made["structuredContent"];
    assert_eq!(made["holder"], "open:hermes", "{made}");
    assert_eq!(made["service"]["created_by"], "open:hermes", "{made}");
    assert_eq!(made["service"]["state"], "asleep", "{made}");
    assert_eq!(made["service"]["subjects"], json!(["echo.one"]), "{made}");
    let id = made["service"]["id"]
        .as_str()
        .ok_or("the record has an id")?
        .to_owned();

    let started = host.tool("start", &json!({"id": id}))?;
    assert_eq!(started["isError"], false, "{started}");
    {
        let (mut registered, _) = ready.accept()?;
        let mut signal = [0];
        registered.read_exact(&mut signal)?;
        assert_eq!(&signal, b"R");
    }
    drop(ready);
    let listed = host.tool("services", &json!({}))?;
    assert_eq!(
        listed["structuredContent"]["services"][0]["state"], "live",
        "{listed}"
    );

    let reply = host.tool(
        "call",
        &json!({"subject": "echo.one", "input": {"text": "hi"}}),
    )?;
    assert_eq!(reply["isError"], false, "{reply}");
    assert_eq!(
        reply["structuredContent"],
        json!({"echoed": {"text": "hi"}}),
        "{reply}"
    );

    let held = host.tool("stop", &json!({"id": id}))?;
    assert_eq!(held["isError"], false, "{held}");
    assert_eq!(
        held["structuredContent"]["service"]["state"], "held",
        "{held}"
    );

    // A seat with no name acts as open:anonymous, and the named seat's list shows its work.
    let anonymous = dir.path().join("anonymous.toml");
    seat_init(&address, None, &anonymous)?;
    let mut other = Host::open(Ticket::spawn(&anonymous)?, true)?;
    let made = other.tool("register", &json!({"template": "echo", "name": "two"}))?;
    assert_eq!(made["isError"], false, "{made}");
    assert_eq!(
        made["structuredContent"]["service"]["created_by"], "open:anonymous",
        "{made}"
    );
    let (stderr, code) = other.ticket.close()?;
    assert_eq!(code, 0, "{stderr}");
    let listed = host.tool("services", &json!({}))?;
    assert_eq!(
        listed["structuredContent"]["services"]
            .as_array()
            .map(Vec::len),
        Some(2),
        "{listed}"
    );

    let retired = host.tool("retire", &json!({"id": id}))?;
    assert_eq!(retired["isError"], false, "{retired}");
    assert_eq!(
        retired["structuredContent"]["service"]["state"], "retired",
        "{retired}"
    );
    let (stderr, code) = host.ticket.close()?;
    assert_eq!(code, 0, "{stderr}");

    let log = server.log()?;
    assert!(log.contains("mode=open"), "{log}");
    Ok(())
}