exocortex-client 0.4.0

The Exocortex MCP client: local cache, WAL offline capture, the session-wrapup tool, and the reconnecting SSE subscriber.
use std::io::Write as _;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt as _;
use std::process::{Command, Stdio};

mod support;

#[test]
#[cfg(unix)]
fn installed_wrapper_starts_supervisor_and_serves_real_mcp_runtime() {
    let dir = std::env::temp_dir().join(format!(
        "exocortex-standalone-wrapper-{}",
        std::process::id()
    ));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();
    let marker = dir.join("supervisor-started");
    let client_args = dir.join("client-args");
    let fake_node = dir.join("exocortex-node");
    let fake_client = dir.join("exocortex-mcp-client");
    let runtime_dir = dir.join("standalone-runtime");
    std::fs::create_dir_all(&runtime_dir).unwrap();
    let redis_server = runtime_dir.join("redis-server");
    let falkor_module = runtime_dir.join("falkordb.so");
    std::fs::write(&redis_server, "#!/bin/sh\nexit 0\n").unwrap();
    std::fs::write(&falkor_module, "fixture").unwrap();
    std::fs::write(
        &fake_node,
        format!(
            "#!/bin/sh\nruntime=''\nwhile [ \"$#\" -gt 0 ]; do\n  if [ \"$1\" = --standalone-runtime-file ]; then runtime=$2; shift 2; else shift; fi\ndone\nprintf '%s\\n%s\\n' \"$EXOCORTEX_REDIS_SERVER\" \"$EXOCORTEX_FALKORDB_MODULE\" > '{}.runtime'\nprintf \"EXOCORTEX_BACKEND='http://127.0.0.1:43119'\\nEXOCORTEX_SSE_KEY='0000000000000000000000000000000000000000000000000000000000000000'\\n\" > \"$runtime\"\ntouch '{}'\ntrap 'exit 0' TERM INT\nwhile :; do sleep 1; done\n",
            marker.display(),
            marker.display()
        ),
    )
    .unwrap();
    std::fs::write(
        &fake_client,
        format!(
            "#!/bin/sh\nprintf '%s\\n' \"$*\" > '{}'\nIFS= read -r request\nprintf '%s\\n' '{{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{{}}}}'\n",
            client_args.display()
        ),
    )
    .unwrap();
    std::fs::set_permissions(&fake_node, std::fs::Permissions::from_mode(0o700)).unwrap();
    std::fs::set_permissions(&fake_client, std::fs::Permissions::from_mode(0o700)).unwrap();
    std::fs::set_permissions(&redis_server, std::fs::Permissions::from_mode(0o700)).unwrap();

    let wrapper = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../scripts/exocortex");
    let mut child = Command::new(wrapper)
        .args([
            "--mode",
            "mcp-standalone",
            "--org",
            "standalone-wrapper",
            "--user",
            "tester",
            "--data-dir",
            dir.to_str().unwrap(),
        ])
        .env("EXOCORTEX_BIN_DIR", &dir)
        .env("EXOCORTEX_STANDALONE_NODE_BIN", &fake_node)
        .env("EXOCORTEX_STANDALONE_CLIENT_BIN", &fake_client)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::inherit())
        .spawn()
        .unwrap();
    let responses = support::BoundedLineReader::new(child.stdout.take().unwrap());
    writeln!(
        child.stdin.as_mut().unwrap(),
        "{}",
        serde_json::json!({
            "jsonrpc": "2.0", "id": 1, "method": "initialize",
            "params": {
                "protocolVersion": "2024-11-05", "capabilities": {},
                "clientInfo": { "name": "wrapper-test", "version": "0" }
            }
        })
    )
    .unwrap();
    child.stdin.as_mut().unwrap().flush().unwrap();
    let response = responses.read_json(&mut child);
    assert!(response.get("result").is_some(), "{response}");
    let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
    while !marker.exists() && std::time::Instant::now() < deadline {
        std::thread::sleep(std::time::Duration::from_millis(10));
    }
    assert!(marker.exists(), "standalone supervisor was not started");
    assert!(child.wait().unwrap().success());
    let args = std::fs::read_to_string(client_args).unwrap();
    assert!(args.contains("--backend http://127.0.0.1:43119"), "{args}");
    let resolved_runtime = std::fs::read_to_string(marker.with_extension("runtime")).unwrap();
    assert_eq!(
        resolved_runtime.lines().collect::<Vec<_>>(),
        [
            redis_server.to_str().unwrap(),
            falkor_module.to_str().unwrap()
        ],
        "an extracted archive must resolve its sibling runtime without overrides"
    );
    std::fs::remove_dir_all(dir).unwrap();
}

#[test]
#[cfg(unix)]
fn installed_wrapper_rule_probe_enters_standalone_topology() {
    let dir = std::env::temp_dir().join(format!(
        "exocortex-standalone-rule-probe-{}",
        std::process::id()
    ));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();
    let marker = dir.join("supervisor-started");
    let fake_node = dir.join("exocortex-node");
    std::fs::write(
        &fake_node,
        format!(
            "#!/bin/sh\nruntime=''\nwhile [ \"$#\" -gt 0 ]; do\n  if [ \"$1\" = --standalone-runtime-file ]; then runtime=$2; shift 2; else shift; fi\ndone\nprintf \"EXOCORTEX_BACKEND='http://127.0.0.1:43119'\\nEXOCORTEX_SSE_KEY='0000000000000000000000000000000000000000000000000000000000000000'\\n\" > \"$runtime\"\ntouch '{}'\ntrap 'exit 0' TERM INT\nwhile :; do sleep 1; done\n",
            marker.display()
        ),
    )
    .unwrap();
    std::fs::set_permissions(&fake_node, std::fs::Permissions::from_mode(0o700)).unwrap();

    let bin_dir = std::path::Path::new(env!("CARGO_BIN_EXE_exocortex-mcp-client"))
        .parent()
        .unwrap();
    let wrapper = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../scripts/exocortex");
    let output = Command::new(wrapper)
        .args([
            "--mode",
            "mcp-standalone",
            "--verify-rules",
            "--org",
            "standalone-rule-probe",
            "--user",
            "tester",
            "--data-dir",
            dir.to_str().unwrap(),
        ])
        .env("EXOCORTEX_BIN_DIR", bin_dir)
        .env("EXOCORTEX_STANDALONE_NODE_BIN", &fake_node)
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "standalone rule probe failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    assert!(marker.exists(), "standalone supervisor was not started");
    assert!(
        String::from_utf8_lossy(&output.stdout).contains("rules-ok mode=mcp-standalone count=9")
    );
    std::fs::remove_dir_all(dir).unwrap();
}

#[test]
#[cfg(unix)]
fn archive_live_harness_leaves_sibling_runtime_resolution_to_wrapper() {
    let dir = std::env::temp_dir().join(format!(
        "exocortex-archive-live-harness-{}",
        std::process::id()
    ));
    let _ = std::fs::remove_dir_all(&dir);
    let runtime = dir.join("standalone-runtime");
    std::fs::create_dir_all(&runtime).unwrap();
    for path in [
        dir.join("exocortex-node"),
        dir.join("exocortex-mcp-client"),
        runtime.join("redis-server"),
    ] {
        std::fs::write(&path, "#!/bin/sh\nexit 0\n").unwrap();
        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700)).unwrap();
    }
    std::fs::write(runtime.join("falkordb.so"), "fixture").unwrap();
    let wrapper = dir.join("exocortex");
    std::fs::write(
        &wrapper,
        "#!/bin/sh\n[ -z \"${EXOCORTEX_REDIS_SERVER:-}\" ]\n[ -z \"${EXOCORTEX_FALKORDB_MODULE:-}\" ]\nprintf '%s\\n' '{\\\"accepted\\\":1,\\\"rejected\\\":0}'\nprintf '%s\\n' 'standalone live durable marker'\n",
    )
    .unwrap();
    std::fs::set_permissions(&wrapper, std::fs::Permissions::from_mode(0o700)).unwrap();

    let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
    let status = Command::new("sh")
        .arg(root.join("scripts/test-standalone-live.sh"))
        .env("EXOCORTEX_BIN_DIR", &dir)
        .env("EXOCORTEX_WRAPPER", &wrapper)
        .env_remove("EXOCORTEX_REDIS_SERVER")
        .env_remove("EXOCORTEX_FALKORDB_MODULE")
        .current_dir(&root)
        .status()
        .unwrap();
    assert!(
        status.success(),
        "the release workflow environment must exercise wrapper-owned sibling runtime discovery"
    );
    std::fs::remove_dir_all(dir).unwrap();
}