liminal-cli 0.3.0

The liminal command: serve the network, maintain its stores, or serve its discovery MCP over stdio
Documentation
//! The ticket uses the network: list, describe, call and flow, each read
//! closing its connection, on the raw wire and on the WebSocket door.

use super::*;

/// The ticket on its seat's transport: the raw wire, or the exchange's
/// WebSocket door when the seat names a `ws://` address (AGT-006 R2), with
/// every tool answering the same.
fn uses_the_network(modern: bool, websocket: bool) -> Result<(), Box<dyn Error>> {
    let persistence_directory = test_persistence::directory()?;
    let server = RunningServer::start(persistence_directory.path())?;
    let (worker, seen) = answering_worker(server.address, 3)?;
    let dir = tempfile::tempdir()?;
    let seat = if websocket {
        server.websocket_url()
    } else {
        server.address.to_string()
    };
    let ticket = Ticket::start(dir.path(), seat, 5_000)?;
    let connections_before = server.active_connections()?;
    let mut host = Host::open(ticket, modern)?;
    // R1: negotiation alone opens nothing on the server.
    assert_eq!(server.active_connections()?, connections_before);

    let tools = host.request("tools/list", json!({}))?;
    tools_list_matches(modern, &tools);

    // R2: list and describe.
    let listed = host.tool("list", &json!({}))?;
    let stations = listed["structuredContent"]["stations"]
        .as_array()
        .cloned()
        .unwrap_or_default();
    let worker_station = stations
        .iter()
        .find(|s| s["subjects"] == json!([SUBJECT]))
        .ok_or("the worker is not listed")?;
    assert_eq!(worker_station["activities"], json!(["echo"]));
    subject_schema_matches(&mut host)?;
    // Nothing is held after a read: the server sees the worker alone.
    assert_eq!(
        server.connections_settled_to(connections_before)?,
        connections_before
    );

    // R3: call.
    let called = host.tool(
        "call",
        &json!({ "subject": SUBJECT, "input": { "text": "hi" } }),
    )?;
    assert_eq!(called["isError"], false, "{called}");
    assert_eq!(
        called["structuredContent"],
        json!({ "echoed": { "text": "hi" } })
    );
    assert_eq!(
        seen.recv_timeout(PUSH_RECV_TIMEOUT)?,
        json!({ "text": "hi" })
    );

    // R4: a two-step flow, the second step fed by the first reply.
    let flowed = host.tool(
        "flow",
        &json!({ "steps": [
            { "subject": SUBJECT, "input": { "text": "a" } },
            { "subject": SUBJECT, "input": { "again": { "$prev": "/echoed/text" } } }
        ] }),
    )?;
    assert_eq!(flowed["isError"], false);
    assert_eq!(
        seen.recv_timeout(PUSH_RECV_TIMEOUT)?,
        json!({ "text": "a" })
    );
    assert_eq!(
        seen.recv_timeout(PUSH_RECV_TIMEOUT)?,
        json!({ "again": "a" })
    );
    assert_eq!(
        flowed["structuredContent"]["steps"][1]["reply"],
        json!({ "echoed": { "again": "a" } })
    );

    // R3: an unserved subject is refused in the server's words.
    let refused = host.tool("call", &json!({ "subject": "nobody.request", "input": {} }))?;
    assert_eq!(refused["isError"], true);
    let words = refused["structuredContent"]["error"]
        .as_str()
        .unwrap_or_default()
        .to_owned();
    assert!(!words.is_empty(), "{refused}");

    // R1: no connection outlives its call.
    assert_eq!(
        server.connections_settled_to(connections_before)?,
        connections_before
    );
    let pass = std::fs::read_to_string(dir.path().join("token"))?
        .trim()
        .to_owned();
    let (stderr, code) = host.ticket.close()?;
    assert_eq!(code, 0, "{stderr}");
    assert!(!stderr.contains(&pass), "the pass reached stderr");
    drop(worker);
    Ok(())
}

#[test]
fn a_modern_host_uses_the_network_through_stdio() -> Result<(), Box<dyn Error>> {
    uses_the_network(true, false)
}

#[test]
fn a_legacy_host_uses_the_network_through_stdio() -> Result<(), Box<dyn Error>> {
    uses_the_network(false, false)
}

#[test]
fn a_host_uses_the_network_through_the_websocket_door() -> Result<(), Box<dyn Error>> {
    uses_the_network(true, true)
}