use std::borrow::Cow;
use rmcp::{model::CallToolRequestParams, transport::TokioChildProcess, ServiceExt};
use serde_json::{json, Value};
fn alc_bin() -> String {
std::env::var("CARGO_BIN_EXE_alc")
.unwrap_or_else(|_| format!("{}/target/debug/alc", env!("CARGO_MANIFEST_DIR")))
}
fn call_params(name: &str, args: Value) -> CallToolRequestParams {
let arguments = match args {
Value::Object(map) => Some(map),
_ => None,
};
let mut p = CallToolRequestParams::default();
p.name = Cow::Owned(name.to_string());
p.arguments = arguments;
p
}
fn extract_text(result: &rmcp::model::CallToolResult) -> &str {
result
.content
.first()
.and_then(|c| c.raw.as_text())
.map(|t| t.text.as_str())
.unwrap_or("")
}
async fn connect_with_home(
alc_home: &std::path::Path,
) -> rmcp::service::RunningService<rmcp::RoleClient, ()> {
let mut cmd = tokio::process::Command::new(alc_bin());
cmd.env("ALC_HOME", alc_home);
let transport = TokioChildProcess::new(cmd).expect("failed to spawn alc server");
().serve(transport)
.await
.expect("failed to initialize MCP session")
}
fn short_tempdir() -> tempfile::TempDir {
let base = if std::path::Path::new("/tmp").is_dir() {
std::path::PathBuf::from("/tmp")
} else {
std::env::temp_dir()
};
tempfile::Builder::new()
.prefix("alcp")
.tempdir_in(base)
.expect("short tempdir")
}
async fn call_json(
client: &rmcp::service::RunningService<rmcp::RoleClient, ()>,
name: &str,
args: Value,
) -> Value {
let result = client
.call_tool(call_params(name, args))
.await
.expect("call_tool failed");
let text = extract_text(&result);
serde_json::from_str(text).unwrap_or_else(|e| panic!("JSON parse failed: {e}\nraw: {text}"))
}
#[tokio::test(flavor = "multi_thread")]
async fn test_pool_paused_session_visible_in_status() {
let tmp = short_tempdir();
let client = connect_with_home(tmp.path()).await;
let resp = call_json(
&client,
"alc_run",
json!({ "code": "return alc.llm('What is 2+2?')", "host_mode": true }),
)
.await;
assert_eq!(
resp["status"], "needs_response",
"host_mode=true run must pause on alc.llm()"
);
let session_id = resp["session_id"]
.as_str()
.expect("session_id must be present")
.to_string();
let status_resp = call_json(&client, "alc_status", json!({ "session_id": session_id })).await;
assert_eq!(
status_resp["status"], "needs_response",
"alc_status must report needs_response for paused pool session"
);
let pool_resp = call_json(&client, "alc_pool_status", json!({})).await;
let sessions = pool_resp["sessions"]
.as_array()
.expect("sessions must be an array");
let found = sessions
.iter()
.any(|s| s["sid"].as_str() == Some(&session_id));
assert!(found, "session_id must appear in alc_pool_status sessions");
let _ = call_json(
&client,
"alc_continue",
json!({ "session_id": session_id, "response": "4" }),
)
.await;
client.cancel().await.expect("cancel failed");
}
#[tokio::test(flavor = "multi_thread")]
async fn test_pool_registry_reconnect_after_mcp_restart() {
let tmp = short_tempdir();
let client_a = connect_with_home(tmp.path()).await;
let resp = call_json(
&client_a,
"alc_run",
json!({ "code": "return alc.llm('ping')", "host_mode": true }),
)
.await;
assert_eq!(resp["status"], "needs_response");
let session_id = resp["session_id"].as_str().expect("session_id").to_string();
client_a.cancel().await.expect("cancel a failed");
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
let client_b = connect_with_home(tmp.path()).await;
let ensure_resp = call_json(&client_b, "alc_pool_ensure", json!({})).await;
let sessions = ensure_resp["sessions"]
.as_array()
.expect("sessions array in pool_ensure response");
let found = sessions
.iter()
.any(|s| s["sid"].as_str() == Some(&session_id));
assert!(
found,
"session_id must still be visible after MCP restart (registry.json reconnect)"
);
let status_resp = call_json(&client_b, "alc_pool_status", json!({})).await;
let pool_sessions = status_resp["sessions"]
.as_array()
.expect("sessions array in pool_status response");
let found_b = pool_sessions
.iter()
.any(|s| s["sid"].as_str() == Some(&session_id));
assert!(
found_b,
"alc_pool_status must report the session as alive after reconnect"
);
let _ = call_json(
&client_b,
"alc_continue",
json!({ "session_id": &session_id, "response": "pong" }),
)
.await;
client_b.cancel().await.expect("cancel b failed");
}
#[tokio::test(flavor = "multi_thread")]
async fn test_pool_continue_after_reconnect() {
let tmp = short_tempdir();
let client_a = connect_with_home(tmp.path()).await;
let resp = call_json(
&client_a,
"alc_run",
json!({ "code": "local r = alc.llm('What is 2+2?') return r", "host_mode": true }),
)
.await;
assert_eq!(resp["status"], "needs_response");
let session_id = resp["session_id"].as_str().expect("session_id").to_string();
client_a.cancel().await.expect("cancel a failed");
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
let client_b = connect_with_home(tmp.path()).await;
let _ = call_json(&client_b, "alc_pool_ensure", json!({})).await;
let cont_resp = call_json(
&client_b,
"alc_continue",
json!({ "session_id": &session_id, "response": "4" }),
)
.await;
let status = cont_resp["status"].as_str().unwrap_or("");
assert!(
status == "completed" || status == "needs_response",
"alc_continue must return completed or needs_response (got {status}); \
session_id={session_id}"
);
client_b.cancel().await.expect("cancel b failed");
}
#[tokio::test(flavor = "multi_thread")]
async fn test_pool_stop_empties_sessions() {
let tmp = short_tempdir();
let client = connect_with_home(tmp.path()).await;
let resp = call_json(
&client,
"alc_run",
json!({ "code": "return alc.llm('stop me')", "host_mode": true }),
)
.await;
assert_eq!(resp["status"], "needs_response");
let stop_resp = call_json(&client, "alc_pool_stop", json!({})).await;
let errors = stop_resp["errors"]
.as_array()
.expect("errors must be an array");
assert!(
errors.is_empty(),
"alc_pool_stop must not produce errors, got: {errors:?}"
);
tokio::time::sleep(tokio::time::Duration::from_millis(300)).await;
let status_resp = call_json(&client, "alc_pool_status", json!({})).await;
let sessions = status_resp["sessions"]
.as_array()
.expect("sessions must be an array");
assert!(
sessions.is_empty(),
"alc_pool_status must return empty sessions after alc_pool_stop, got: {sessions:?}"
);
client.cancel().await.expect("cancel failed");
}